From c99bfda01545d9dabc5425e67a7abf5ca5877459 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sat, 18 Feb 2023 21:01:31 +0100 Subject: [PATCH 001/589] diva: added all previous commits, added username and password change - Changed `update_profile()` function to allow a single Dict instead of multiple values - Added `passwd*` Columns to profile table and added corresponding update/rollback sql scripts - Added `handle_card_procedure_request()`, `handle_change_name_request ()` and `handle_change_passwd_request()` functions to DivaBase --- core/data/schema/versions/SBZV_1_rollback.sql | 9 + core/data/schema/versions/SBZV_2_rollback.sql | 17 + core/data/schema/versions/SBZV_2_upgrade.sql | 9 + core/data/schema/versions/SBZV_3_rollback.sql | 2 + core/data/schema/versions/SBZV_3_upgrade.sql | 33 ++ core/data/schema/versions/SBZV_4_upgrade.sql | 2 + example_config/diva.yaml | 3 + titles/diva/base.py | 395 ++++++++++++++---- titles/diva/config.py | 22 +- titles/diva/database.py | 6 +- titles/diva/schema/__init__.py | 7 +- titles/diva/schema/customize.py | 63 +++ titles/diva/schema/item.py | 24 +- titles/diva/schema/module.py | 63 +++ titles/diva/schema/profile.py | 102 +++-- titles/diva/schema/pv_customize.py | 69 +++ titles/diva/schema/score.py | 146 +++++-- titles/diva/schema/static.py | 42 +- 18 files changed, 824 insertions(+), 190 deletions(-) create mode 100644 core/data/schema/versions/SBZV_1_rollback.sql create mode 100644 core/data/schema/versions/SBZV_2_rollback.sql create mode 100644 core/data/schema/versions/SBZV_2_upgrade.sql create mode 100644 core/data/schema/versions/SBZV_3_rollback.sql create mode 100644 core/data/schema/versions/SBZV_3_upgrade.sql create mode 100644 core/data/schema/versions/SBZV_4_upgrade.sql create mode 100644 titles/diva/schema/customize.py create mode 100644 titles/diva/schema/module.py create mode 100644 titles/diva/schema/pv_customize.py diff --git a/core/data/schema/versions/SBZV_1_rollback.sql b/core/data/schema/versions/SBZV_1_rollback.sql new file mode 100644 index 0000000..a7bccce --- /dev/null +++ b/core/data/schema/versions/SBZV_1_rollback.sql @@ -0,0 +1,9 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE diva_score DROP 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; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_2_rollback.sql b/core/data/schema/versions/SBZV_2_rollback.sql new file mode 100644 index 0000000..5bd46c0 --- /dev/null +++ b/core/data/schema/versions/SBZV_2_rollback.sql @@ -0,0 +1,17 @@ +ALTER TABLE diva_profile_shop DROP COLUMN c_itm_eqp_ary; +ALTER TABLE diva_profile_shop DROP COLUMN ms_itm_flg_ary; + +ALTER TABLE diva_profile DROP COLUMN use_pv_mdl_eqp; +ALTER TABLE diva_profile DROP COLUMN use_mdl_pri; +ALTER TABLE diva_profile DROP COLUMN use_pv_skn_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_btn_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_chn_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sldr_tch_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_mdl_eqp VARCHAR(8) NOT NULL DEFAULT "true" AFTER sort_kind; +ALTER TABLE diva_profile ADD COLUMN use_pv_btn_se_eqp VARCHAR(8) NOT NULL DEFAULT "true" AFTER use_pv_mdl_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sld_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_btn_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_chn_sld_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_sld_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sldr_tch_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_chn_sld_se_eqp; + +DROP TABLE IF EXISTS `diva_profile_pv_customize`; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_2_upgrade.sql b/core/data/schema/versions/SBZV_2_upgrade.sql new file mode 100644 index 0000000..26cb2ed --- /dev/null +++ b/core/data/schema/versions/SBZV_2_upgrade.sql @@ -0,0 +1,9 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE diva_score ADD COLUMN edition int(11) DEFAULT 0 AFTER difficulty; +ALTER TABLE diva_playlog ADD COLUMN edition int(11) DEFAULT 0 AFTER difficulty; + +ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; +ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; +ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty, edition); +ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_3_rollback.sql b/core/data/schema/versions/SBZV_3_rollback.sql new file mode 100644 index 0000000..00d95e1 --- /dev/null +++ b/core/data/schema/versions/SBZV_3_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile DROP COLUMN passwd_stat; +ALTER TABLE diva_profile DROP COLUMN passwd; diff --git a/core/data/schema/versions/SBZV_3_upgrade.sql b/core/data/schema/versions/SBZV_3_upgrade.sql new file mode 100644 index 0000000..a06d7f4 --- /dev/null +++ b/core/data/schema/versions/SBZV_3_upgrade.sql @@ -0,0 +1,33 @@ +ALTER TABLE diva_profile_shop ADD COLUMN c_itm_eqp_ary varchar(59) DEFAULT "-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999"; +ALTER TABLE diva_profile_shop ADD COLUMN ms_itm_flg_ary varchar(59) DEFAULT "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"; + +ALTER TABLE diva_profile DROP COLUMN use_pv_mdl_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_btn_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_chn_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sldr_tch_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_mdl_eqp BOOLEAN NOT NULL DEFAULT true AFTER sort_kind; +ALTER TABLE diva_profile ADD COLUMN use_mdl_pri BOOLEAN NOT NULL DEFAULT false AFTER use_pv_mdl_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_skn_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_mdl_pri; +ALTER TABLE diva_profile ADD COLUMN use_pv_btn_se_eqp BOOLEAN NOT NULL DEFAULT true AFTER use_pv_skn_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sld_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_btn_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_chn_sld_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_sld_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sldr_tch_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_chn_sld_se_eqp; + + +CREATE TABLE diva_profile_pv_customize ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + version INT NOT NULL, + pv_id INT NOT NULL, + mdl_eqp_ary VARCHAR(14) DEFAULT '-999,-999,-999', + c_itm_eqp_ary VARCHAR(59) DEFAULT '-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999', + ms_itm_flg_ary VARCHAR(59) DEFAULT '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', + skin INT DEFAULT '-1', + btn_se INT DEFAULT '-1', + sld_se INT DEFAULT '-1', + chsld_se INT DEFAULT '-1', + sldtch_se INT DEFAULT '-1', + UNIQUE KEY diva_profile_pv_customize_uk (user, version, pv_id), + CONSTRAINT diva_profile_pv_customize_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/core/data/schema/versions/SBZV_4_upgrade.sql b/core/data/schema/versions/SBZV_4_upgrade.sql new file mode 100644 index 0000000..d3aacb6 --- /dev/null +++ b/core/data/schema/versions/SBZV_4_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile ADD COLUMN passwd_stat INTEGER NOT NULL DEFAULT 0; +ALTER TABLE diva_profile ADD COLUMN passwd VARCHAR(12) NOT NULL DEFAULT "**********"; diff --git a/example_config/diva.yaml b/example_config/diva.yaml index 1354910..ad1842a 100644 --- a/example_config/diva.yaml +++ b/example_config/diva.yaml @@ -2,3 +2,6 @@ server: enable: True loglevel: "info" +mods: + unlock_all_modules: True + unlock_all_items: True diff --git a/titles/diva/base.py b/titles/diva/base.py index aef5841..7ad5793 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -127,32 +127,54 @@ class DivaBase(): def handle_shop_catalog_request(self, data: Dict) -> Dict: catalog = "" - shopList = self.data.static.get_enabled_shop(self.version) + shopList = self.data.static.get_enabled_shops(self.version) if not shopList: with open(r"titles/diva/data/ShopCatalog.dat", encoding="utf-8") as shop: lines = shop.readlines() for line in lines: line = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" - catalog = catalog.replace("+", "%20") - response = "" - response += f"&shp_ctlg_lut={self.time_lut}" - response += f"&shp_ctlg={catalog[:-3]}" 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 = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" - catalog = catalog.replace("+", "%20") - - response = "" - response += f"&shp_ctlg_lut={self.time_lut}" - response += f"&shp_ctlg={catalog[:-3]}" + catalog = catalog.replace("+", "%20") + + response = f"&shp_ctlg_lut={self.time_lut}" + response += f"&shp_ctlg={catalog[:-3]}" return ( response ) + def handle_buy_module_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) + module = self.data.static.get_enabled_shop(self.version, int(data["mdl_id"])) + + # make sure module is available to purchase + if not module: + return f"&shp_rslt=0&vcld_pts={profile['vcld_pts']}" + + # make sure player has enough vocaloid points to buy module + if profile["vcld_pts"] < int(data["mdl_price"]): + return f"&shp_rslt=0&vcld_pts={profile['vcld_pts']}" + + new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"]) + + self.data.profile.update_profile(data["pd_id"], profile["lv_num"], profile["lv_pnt"], new_vcld_pts, profile["hp_vol"], profile["btn_se_vol"], profile["btn_se_vol2"], profile["sldr_se_vol2"], profile["sort_kind"], profile["use_pv_mdl_eqp"], profile["use_mdl_pri"], profile["use_pv_skn_eqp"], profile["use_pv_btn_se_eqp"], profile["use_pv_sld_se_eqp"], profile["use_pv_chn_sld_se_eqp"], profile["use_pv_sldr_tch_se_eqp"], profile["nxt_pv_id"], profile["nxt_dffclty"], profile["nxt_edtn"], profile["dsp_clr_brdr"], profile["dsp_intrm_rnk"], profile["dsp_clr_sts"], profile["rgo_sts"], profile["lv_efct_id"], profile["lv_plt_id"], profile["my_qst_id"], profile["my_qst_sts"]) + self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"]) + + # generate the mdl_have string + mdl_have = self.data.module.get_modules_have_string(data["pd_id"], self.version) + + response = "&shp_rslt=1" + response += f"&mdl_id={data['mdl_id']}" + response += f"&mdl_have={mdl_have}" + response += f"&vcld_pts={new_vcld_pts}" + + return response + def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: catalog = "" @@ -163,25 +185,51 @@ class DivaBase(): for line in lines: line = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" - catalog = catalog.replace("+", "%20") - response = "" - response += f"&cstmz_itm_ctlg_lut={self.time_lut}" - response += f"&cstmz_itm_ctlg={catalog[:-3]}" 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 = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" - catalog = catalog.replace("+", "%20") + catalog = catalog.replace("+", "%20") - response = "" - response += f"&cstmz_itm_ctlg_lut={self.time_lut}" - response += f"&cstmz_itm_ctlg={catalog[:-3]}" + response = f"&cstmz_itm_ctlg_lut={self.time_lut}" + response += f"&cstmz_itm_ctlg={catalog[:-3]}" 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"])) + + # make sure module is available to purchase + if not item: + return f"&shp_rslt=0&vcld_pts={profile['vcld_pts']}" + + # make sure player has enough vocaloid points to buy the customize item + if profile["vcld_pts"] < int(data["cstmz_itm_price"]): + return f"&shp_rslt=0&vcld_pts={profile['vcld_pts']}" + + new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"]) + + # save new Vocaloid Points balance + profile = dict(profile) + profile["vcld_pts"] = new_vcld_pts + self.data.profile.update_profile(profile) + + 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) + + response = "&shp_rslt=1" + response += f"&cstmz_itm_id={data['cstmz_itm_id']}" + response += f"&cstmz_itm_have={cstmz_itm_have}" + response += f"&vcld_pts={new_vcld_pts}" + + return response + def handle_festa_info_request(self, data: Dict) -> Dict: encoded = "&" params = { @@ -191,7 +239,7 @@ class DivaBase(): 'fi_difficulty': '-1,-1', 'fi_pv_id_lst': 'ALL,ALL', 'fi_attr': '7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - 'fi_add_vp': '10,0', + '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', @@ -278,48 +326,68 @@ class DivaBase(): def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: return ( f'' ) - def handle_pre_start_request(self, data: Dict) -> Dict: + def handle_pre_start_request(self, data: Dict) -> str: profile = self.data.profile.get_profile(data["aime_id"], self.version) profile_shop = self.data.item.get_shop(data["aime_id"], self.version) + if profile is None: - return ( f"&ps_result=-3") + return f"&ps_result=-3" else: - response = "" - response += "&ps_result=1" - response += f"&pd_id={data['aime_id']}" + response = "&ps_result=1" + response += "&accept_idx=100" response += "&nblss_ltt_stts=-1" response += "&nblss_ltt_tckt=-1" response += "&nblss_ltt_is_opn=-1" - response += f"&vcld_pts={profile['vcld_pts']}" + response += f"&pd_id={data['aime_id']}" response += f"&player_name={profile['player_name']}" + response += f"&sort_kind={profile['player_name']}" response += f"&lv_efct_id={profile['lv_efct_id']}" response += f"&lv_plt_id={profile['lv_plt_id']}" response += f"&lv_str={profile['lv_str']}" response += f"&lv_num={profile['lv_num']}" response += f"&lv_pnt={profile['lv_pnt']}" + response += f"&vcld_pts={profile['vcld_pts']}" + response += f"&skn_eqp={profile['use_pv_skn_eqp']}" + response += f"&btn_se_eqp={profile['use_pv_btn_se_eqp']}" + response += f"&sld_se_eqp={profile['use_pv_sld_se_eqp']}" + response += f"&chn_sld_se_eqp={profile['use_pv_chn_sld_se_eqp']}" + response += f"&sldr_tch_se_eqp={profile['use_pv_sldr_tch_se_eqp']}" + response += f"&passwd_stat={profile['passwd_stat']}" - #Store stuff to add to rework + # Store stuff to add to rework response += f"&mdl_eqp_tm={self.time_lut}" + mdl_eqp_ary = "-999,-999,-999" + + # get the common_modules from the profile shop if profile_shop: - response += f"&mdl_eqp_ary={profile_shop['mdl_eqp_ary']}" + mdl_eqp_ary = profile_shop["mdl_eqp_ary"] - response += f"&c_itm_eqp_ary=-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999" - response += f"&ms_itm_flg_ary=1,1,1,1,1,1,1,1,1,1,1,1" + response += f"&mdl_eqp_ary={mdl_eqp_ary}" - return ( response ) - - def handle_registration_request(self, data: Dict) -> Dict: #DONE + 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']}") - + 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) profile_shop = self.data.item.get_shop(data["pd_id"], self.version) - if profile is None: return - - response = "" - response += f"&pd_id={data['pd_id']}" + if profile is None: + return + + mdl_have = "F" * 250 + # generate the mdl_have string if "unlock_all_modules" is disabled + if not self.game_config.mods.unlock_all_modules: + mdl_have = 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) + + response = f"&pd_id={data['pd_id']}" response += "&start_result=1" response += "&accept_idx=100" @@ -333,13 +401,15 @@ class DivaBase(): response += f"&lv_pnt={profile['lv_pnt']}" response += f"&lv_efct_id={profile['lv_efct_id']}" response += f"&lv_plt_id={profile['lv_plt_id']}" - response += "&mdl_have=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - response += "&cstmz_itm_have=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - response += f"&use_pv_mdl_eqp={profile['use_pv_mdl_eqp']}" - response += f"&use_pv_btn_se_eqp={profile['use_pv_btn_se_eqp']}" - response += f"&use_pv_sld_se_eqp={profile['use_pv_sld_se_eqp']}" - response += f"&use_pv_chn_sld_se_eqp={profile['use_pv_chn_sld_se_eqp']}" - response += f"&use_pv_sldr_tch_se_eqp={profile['use_pv_sldr_tch_se_eqp']}" + response += f"&mdl_have={mdl_have}" + response += f"&cstmz_itm_have={cstmz_itm_have}" + response += f"&use_pv_mdl_eqp={int(profile['use_pv_mdl_eqp'])}" + response += f"&use_mdl_pri={int(profile['use_mdl_pri'])}" + response += f"&use_pv_skn_eqp={int(profile['use_pv_skn_eqp'])}" + response += f"&use_pv_btn_se_eqp={int(profile['use_pv_btn_se_eqp'])}" + response += f"&use_pv_sld_se_eqp={int(profile['use_pv_sld_se_eqp'])}" + response += f"&use_pv_chn_sld_se_eqp={int(profile['use_pv_chn_sld_se_eqp'])}" + response += f"&use_pv_sldr_tch_se_eqp={int(profile['use_pv_sldr_tch_se_eqp'])}" response += f"&vcld_pts={profile['lv_efct_id']}" response += f"&nxt_pv_id={profile['nxt_pv_id']}" response += f"&nxt_dffclty={profile['nxt_dffclty']}" @@ -349,7 +419,7 @@ class DivaBase(): response += f"&dsp_clr_sts={profile['dsp_clr_sts']}" response += f"&rgo_sts={profile['rgo_sts']}" - #To be fully fixed + # To be fully fixed if "my_qst_id" not in profile: response += f"&my_qst_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" response += f"&my_qst_sts=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" @@ -361,14 +431,23 @@ class DivaBase(): response += f"&my_qst_et=2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2100-01-01%2008%3A59%3A59.0,2100-01-01%2008%3A59%3A59.0,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx" response += f"&clr_sts=0,0,0,0,0,0,0,0,56,52,35,6,6,3,1,0,0,0,0,0" - #Store stuff to add to rework + # Store stuff to add to rework response += f"&mdl_eqp_tm={self.time_lut}" - if profile_shop: - response += f"&mdl_eqp_ary={profile_shop['mdl_eqp_ary']}" + mdl_eqp_ary = "-999,-999,-999" + c_itm_eqp_ary = "-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999" + ms_itm_flg_ary = "1,1,1,1,1,1,1,1,1,1,1,1" - response += f"&c_itm_eqp_ary=-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999" - response += f"&ms_itm_flg_ary=1,1,1,1,1,1,1,1,1,1,1,1" + # get the common_modules, customize_items and customize_item_flags + # from the profile shop + if profile_shop: + mdl_eqp_ary = profile_shop["mdl_eqp_ary"] + c_itm_eqp_ary = profile_shop["c_itm_eqp_ary"] + ms_itm_flg_ary = profile_shop["ms_itm_flg_ary"] + + 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 ) @@ -390,22 +469,89 @@ class DivaBase(): 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: + """ + Helper function to generate the pv_result string for every song, ranking and edition + """ + global_ranking = -1 + if pd_db_ranking: + # 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 + pv_result += f"{edition}," + # rslt + pv_result += f"{pd_db_song['clr_kind']}," if pd_db_song else "-1," + # max_score + pv_result += f"{pd_db_song['score']}," if pd_db_song else "-1," + # max_atn_pnt + pv_result += f"{pd_db_song['atn_pnt']}," if pd_db_song else "-1," + # challenge_kind + pv_result += f"{pd_db_song['sort_kind']}," if pd_db_song else "0," + + module_eqp = "-999,-999,-999" + customize_eqp = "-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999" + customize_flag = "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1" + # skin, btn_se, sld_se, chsld_se, sldtch_se + pv_settings = "-1,-1,-1,-1,-1" + if pd_db_customize: + module_eqp = pd_db_customize["mdl_eqp_ary"] + customize_eqp = pd_db_customize["c_itm_eqp_ary"] + customize_flag = pd_db_customize["ms_itm_flg_ary"] + pv_settings = ( + f"{pd_db_customize['skin']}," + f"{pd_db_customize['btn_se']}," + f"{pd_db_customize['sld_se']}," + 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}," + pv_result += f"{pv_settings}," + # rvl_pd_id, rvl_score, rvl_attn_pnt, -1, -1 + pv_result += "-1,-1,-1,-1,-1," + # countrywide_ranking + pv_result += f"{global_ranking}," + # rgo_purchased + pv_result += "1,1,1," + # rgo_played + pv_result += "0,0,0" + + return pv_result + def handle_get_pv_pd_request(self, data: Dict) -> Dict: song_id = data["pd_pv_id_lst"].split(",") pv = "" for song in song_id: if int(song) > 0: - pd_db_song = self.data.score.get_best_score(data["pd_id"], int(song), data["difficulty"]) - if pd_db_song is not None: - - pv += urllib.parse.quote(f"{song},0,{pd_db_song['clr_kind']},{pd_db_song['score']},{pd_db_song['atn_pnt']},{pd_db_song['sort_kind']},-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1337,1,1,1,0,0,0") - - else: - #self.logger.debug(f"No score saved for ID: {song}!") - pv += urllib.parse.quote(f"{song},0,-1,-1,-1,0,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1,1,1,0,0,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) - #pv_no, edition, rslt, max_score, max_atn_pnt, challenge_kind, module_eqp[-999,-999,-999], customize_eqp[-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999], customize_flag[1,1,1,1,1,1,1,1,1,1,1,1], skin, btn_se, sld_se, chsld_se, sldtch_se, rvl_pd_id, rvl_score, rvl_attn_pnt, countrywide_ranking, rgo_hispeed, rgo_hidden, rgo_sudden, rgo_hispeed_cleared, rgo_hidden_cleared, rgo_sudden_cleared, chain_challenge_num, chain_challenge_max, chain_challenge_open, version + 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) + + 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_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) + + self.logger.debug(f"pv_result = {pv_result}") + + pv += urllib.parse.quote(pv_result) else: pv += urllib.parse.quote(f"{song}***") pv += "," @@ -426,6 +572,7 @@ class DivaBase(): pd_song_list = data["stg_ply_pv_id"].split(",") pd_song_difficulty = data["stg_difficulty"].split(",") + pd_song_edition = data["stg_edtn"].split(",") pd_song_max_score = data["stg_score"].split(",") pd_song_max_atn_pnt = data["stg_atn_pnt"].split(",") pd_song_ranking = data["stg_clr_kind"].split(",") @@ -439,31 +586,52 @@ 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_score(data["pd_id"], pd_song_list[index], pd_song_difficulty[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_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_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_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_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_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 - old_level = int(profile['lv_num']) - new_level = (int(data["ttl_vp_add"]) + int(profile["lv_pnt"])) / 12 + # Calculate new level + best_scores = self.data.score.get_best_scores(data["pd_id"]) - self.data.profile.update_profile(data["pd_id"], int(new_level), int(profile["lv_pnt"]) + int(data["ttl_vp_add"]), int(data["vcld_pts"]), int(data["hp_vol"]), int(data["btn_se_vol"]), int(data["btn_se_vol2"]), int(data["sldr_se_vol2"]), int(data["sort_kind"]), int(data["use_pv_mdl_eqp"]), profile["use_pv_btn_se_eqp"], profile["use_pv_sld_se_eqp"], profile["use_pv_chn_sld_se_eqp"], profile["use_pv_sldr_tch_se_eqp"], int(data["ply_pv_id"]), int(data["nxt_dffclty"]), int(data["nxt_edtn"]), profile["dsp_clr_brdr"], profile["dsp_intrm_rnk"], profile["dsp_clr_sts"], profile["rgo_sts"], profile["lv_efct_id"], profile["lv_plt_id"], data["my_qst_id"], data["my_qst_sts"]) + 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) - response = "" - - response += "&chllng_kind=-1" - response += f"&lv_num_old={int(old_level)}" + response = "&chllng_kind=-1" + response += f"&lv_num_old={int(profile['lv_num'])}" response += f"&lv_pnt_old={int(profile['lv_pnt'])}" - response += f"&lv_num={int(profile['lv_num'])}" + + # update the profile and commit changes to the db + profile = dict(profile) + profile["lv_num"] = new_level + profile["lv_pnt"] = new_level_pnt + profile["vcld_pts"] = int(data["vcld_pts"]) + profile["hp_vol"] = int(data["hp_vol"]) + profile["btn_se_vol"] = int(data["btn_se_vol"]) + profile["sldr_se_vol2"] = int(data["sldr_se_vol2"]) + profile["sort_kind"] = int(data["sort_kind"]) + profile["nxt_pv_id"] = int(data["ply_pv_id"]) + profile["nxt_dffclty"] = int(data["nxt_dffclty"]) + profile["nxt_edtn"] = int(data["nxt_edtn"]) + profile["my_qst_id"] = data["my_qst_id"] + profile["my_qst_sts"] = data["my_qst_sts"] + + self.data.profile.update_profile(profile) + + response += f"&lv_num={new_level}" response += f"&lv_str={profile['lv_str']}" - response += f"&lv_pnt={int(profile['lv_pnt']) + int(data['ttl_vp_add'])}" + response += f"&lv_pnt={new_level_pnt}" response += f"&lv_efct_id={int(profile['lv_efct_id'])}" response += f"&lv_plt_id={int(profile['lv_plt_id'])}" response += f"&vcld_pts={int(data['vcld_pts'])}" @@ -495,12 +663,79 @@ class DivaBase(): def handle_end_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) - self.data.profile.update_profile(data["pd_id"], profile["lv_num"], profile["lv_pnt"], profile["vcld_pts"], profile["hp_vol"], profile["btn_se_vol"], profile["btn_se_vol2"], profile["sldr_se_vol2"], profile["sort_kind"], profile["use_pv_mdl_eqp"], profile["use_pv_btn_se_eqp"], profile["use_pv_sld_se_eqp"], profile["use_pv_chn_sld_se_eqp"], profile["use_pv_sldr_tch_se_eqp"], profile["nxt_pv_id"], profile["nxt_dffclty"], profile["nxt_edtn"], profile["dsp_clr_brdr"], profile["dsp_intrm_rnk"], profile["dsp_clr_sts"], profile["rgo_sts"], profile["lv_efct_id"], profile["lv_plt_id"], data["my_qst_id"], data["my_qst_sts"]) - return ( f'' ) + + profile = dict(profile) + profile["my_qst_id"] = data["my_qst_id"] + profile["my_qst_sts"] = data["my_qst_sts"] + + self.data.profile.update_profile(profile) + 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"]) + 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"]) + 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") - response = "" - response += "&shp_rslt=1" + response = "&shp_rslt=1" 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" + response += f"&pd_id={profile['user']}" + response += f"&player_name={profile['player_name']}" + response += f"&lv_num={profile['lv_num']}" + response += f"&lv_pnt={profile['lv_pnt']}" + response += f"&lv_str={profile['lv_str']}" + response += f"&lv_efct_id={profile['lv_efct_id']}" + response += f"&lv_plt_id={profile['lv_plt_id']}" + response += f"&vcld_pts={profile['vcld_pts']}" + 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 + profile = dict(profile) + profile["vcld_pts"] -= int(data["chg_name_price"]) + profile["player_name"] = data["player_name"] + + self.data.profile.update_profile(profile) + + response = "&cd_adm_result=1" + response += "&accept_idx=100" + response += f"&pd_id={profile['user']}" + response += f"&player_name={profile['player_name']}" + + return response + + def handle_change_passwd_request(self, data: Dict) -> str: + profile = self.data.profile.get_profile(data["pd_id"], self.version) + + # set password to true and update the saved password + profile = dict(profile) + profile["passwd_stat"] = 1 + profile["passwd"] = data["new_passwd"] + + self.data.profile.update_profile(profile) + + response = "&cd_adm_result=1" + response += "&accept_idx=100" + response += f"&pd_id={profile['user']}" + + return response diff --git a/titles/diva/config.py b/titles/diva/config.py index aba99f2..af4d626 100644 --- a/titles/diva/config.py +++ b/titles/diva/config.py @@ -1,17 +1,33 @@ from core.config import CoreConfig + 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) - + @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'diva', 'server', 'loglevel', default="info")) + +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) + + @property + def unlock_all_items(self) -> bool: + return CoreConfig.get_config_field(self.__config, 'diva', 'mods', 'unlock_all_items', default=True) + + class DivaConfig(dict): def __init__(self) -> None: - self.server = DivaServerConfig(self) \ No newline at end of file + self.server = DivaServerConfig(self) + self.mods = DivaModsConfig(self) diff --git a/titles/diva/database.py b/titles/diva/database.py index f43c42e..a7e4193 100644 --- a/titles/diva/database.py +++ b/titles/diva/database.py @@ -1,6 +1,7 @@ from core.data import Data from core.config import CoreConfig -from titles.diva.schema import DivaProfileData, DivaScoreData, DivaItemData, DivaStaticData +from titles.diva.schema import DivaProfileData, DivaScoreData, DivaModuleData, DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData, DivaStaticData + class DivaData(Data): def __init__(self, cfg: CoreConfig) -> None: @@ -8,5 +9,8 @@ class DivaData(Data): self.profile = DivaProfileData(self.config, self.session) self.score = DivaScoreData(self.config, self.session) + self.module = DivaModuleData(self.config, self.session) + self.customize = DivaCustomizeItemData(self.config, self.session) + self.pv_customize = DivaPvCustomizeData(self.config, self.session) self.item = DivaItemData(self.config, self.session) self.static = DivaStaticData(self.config, self.session) diff --git a/titles/diva/schema/__init__.py b/titles/diva/schema/__init__.py index 57431ef..72cd97f 100644 --- a/titles/diva/schema/__init__.py +++ b/titles/diva/schema/__init__.py @@ -1,6 +1,11 @@ from titles.diva.schema.profile import DivaProfileData from titles.diva.schema.score import DivaScoreData +from titles.diva.schema.module import DivaModuleData +from titles.diva.schema.customize import DivaCustomizeItemData +from titles.diva.schema.pv_customize import DivaPvCustomizeData from titles.diva.schema.item import DivaItemData from titles.diva.schema.static import DivaStaticData -__all__ = [DivaProfileData, DivaScoreData, DivaItemData, DivaStaticData] +__all__ = [DivaProfileData, DivaScoreData, DivaModuleData, + DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData, + DivaStaticData] diff --git a/titles/diva/schema/customize.py b/titles/diva/schema/customize.py new file mode 100644 index 0000000..f372349 --- /dev/null +++ b/titles/diva/schema/customize.py @@ -0,0 +1,63 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +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("version", Integer, nullable=False), + Column("item_id", Integer, nullable=False), + 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 + ) + + 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}") + return None + return result.lastrowid + + def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]: + """ + Given a game version and an aime id, return all the customize items, not used directly + """ + sql = customize.select(and_( + customize.c.version == version, + customize.c.user == aime_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_customize_items_have_string(self, aime_id: int, version: int) -> str: + """ + Given a game version and an aime id, return the cstmz_itm_have hex string + required for diva directly + """ + items_list = self.get_customize_items(aime_id, version) + if items_list is None: + items_list = [] + item_have = 0 + + for item in items_list: + item_have |= 1 << item["item_id"] + + # convert the int to a 250 digit long hex string + return "{0:0>250}".format(hex(item_have).upper()[2:]) diff --git a/titles/diva/schema/item.py b/titles/diva/schema/item.py index 96d8ca5..ce7d910 100644 --- a/titles/diva/schema/item.py +++ b/titles/diva/schema/item.py @@ -14,20 +14,29 @@ shop = Table( 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' ) -class DivaItemData(BaseData): - def put_shop(self, aime_id: int, version: int, mdl_eqp_ary: str) -> None: + +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: + sql = insert(shop).values( version=version, user=aime_id, - mdl_eqp_ary=mdl_eqp_ary + mdl_eqp_ary=mdl_eqp_ary, + c_itm_eqp_ary=c_itm_eqp_ary, + ms_itm_flg_ary=ms_itm_flg_ary ) conflict = sql.on_duplicate_key_update( - mdl_eqp_ary = sql.inserted.mdl_eqp_ary + mdl_eqp_ary=mdl_eqp_ary, + c_itm_eqp_ary=c_itm_eqp_ary, + ms_itm_flg_ary=ms_itm_flg_ary ) result = self.execute(conflict) @@ -44,7 +53,8 @@ class DivaItemData(BaseData): shop.c.version == version, shop.c.user == aime_id )) - + result = self.execute(sql) - if result is None: return None - return result.fetchone() \ No newline at end of file + if result is None: + return None + return result.fetchone() diff --git a/titles/diva/schema/module.py b/titles/diva/schema/module.py new file mode 100644 index 0000000..f9c930c --- /dev/null +++ b/titles/diva/schema/module.py @@ -0,0 +1,63 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +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("version", Integer, nullable=False), + Column("module_id", Integer, nullable=False), + UniqueConstraint("user", "version", "module_id", name="diva_profile_module_uk"), + 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 + ) + + 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}") + return None + return result.lastrowid + + def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]: + """ + Given a game version and an aime id, return all the modules, not used directly + """ + sql = module.select(and_( + module.c.version == version, + module.c.user == aime_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_modules_have_string(self, aime_id: int, version: int) -> str: + """ + Given a game version and an aime id, return the mdl_have hex string + required for diva directly + """ + module_list = self.get_modules(aime_id, version) + if module_list is None: + module_list = [] + module_have = 0 + + for module in module_list: + module_have |= 1 << module["module_id"] + + # convert the int to a 250 digit long hex string + return "{0:0>250}".format(hex(module_have).upper()[2:]) diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 72d68ed..76e8241 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -11,7 +11,8 @@ 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(8), nullable=False), Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"), @@ -23,11 +24,15 @@ profile = Table( Column("btn_se_vol2", Integer, nullable=False, server_default="100"), Column("sldr_se_vol2", Integer, nullable=False, server_default="100"), Column("sort_kind", Integer, nullable=False, server_default="2"), - Column("use_pv_mdl_eqp", String(8), nullable=False, server_default="true"), - Column("use_pv_btn_se_eqp", String(8), nullable=False, server_default="true"), - Column("use_pv_sld_se_eqp", String(8), nullable=False, server_default="false"), - Column("use_pv_chn_sld_se_eqp", String(8), nullable=False, server_default="false"), - Column("use_pv_sldr_tch_se_eqp", String(8), nullable=False, server_default="false"), + Column("use_pv_mdl_eqp", Boolean, nullable=False, server_default="1"), + Column("use_mdl_pri", Boolean, nullable=False, server_default="0"), + 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("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"), @@ -37,14 +42,20 @@ profile = Table( Column("rgo_sts", Integer, nullable=False, server_default="1"), Column("lv_efct_id", Integer, nullable=False, server_default="0"), Column("lv_plt_id", Integer, nullable=False, server_default="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"), + 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"), UniqueConstraint("user", "version", name="diva_profile_uk"), 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 """ @@ -55,48 +66,46 @@ class DivaProfileData(BaseData): ) conflict = sql.on_duplicate_key_update( - player_name = sql.inserted.player_name + 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}") + self.logger.error( + f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}") return None return result.lastrowid - def update_profile(self, profile_id: int, lv_num: int, lv_pnt: int, vcld_pts: int, hp_vol: int, btn_se_vol: int, btn_se_vol2: int, sldr_se_vol2: int, sort_kind: int, use_pv_mdl_eqp: str, use_pv_btn_se_eqp: str, use_pv_sld_se_eqp: str, use_pv_chn_sld_se_eqp: str, use_pv_sldr_tch_se_eqp: str, nxt_pv_id: int, nxt_dffclty: int, nxt_edtn: int, dsp_clr_brdr: int, dsp_intrm_rnk: int, dsp_clr_sts: int, rgo_sts: int, lv_efct_id: int, lv_plt_id: int, my_qst_id: str, my_qst_sts: str) -> None: - sql = profile.update(profile.c.user == profile_id).values( - - lv_num = lv_num, - lv_pnt = lv_pnt, - vcld_pts = vcld_pts, - hp_vol = hp_vol, - btn_se_vol = btn_se_vol, - btn_se_vol2 = btn_se_vol2, - sldr_se_vol2 = sldr_se_vol2, - sort_kind = sort_kind, - use_pv_mdl_eqp = use_pv_mdl_eqp, - use_pv_btn_se_eqp = use_pv_btn_se_eqp, - use_pv_sld_se_eqp = use_pv_sld_se_eqp, - use_pv_chn_sld_se_eqp = use_pv_chn_sld_se_eqp, - use_pv_sldr_tch_se_eqp = use_pv_sldr_tch_se_eqp, - nxt_pv_id = nxt_pv_id, - nxt_dffclty = nxt_dffclty, - nxt_edtn = nxt_edtn, - dsp_clr_brdr = dsp_clr_brdr, - dsp_intrm_rnk = dsp_intrm_rnk, - dsp_clr_sts = dsp_clr_sts, - rgo_sts = rgo_sts, - lv_efct_id = lv_efct_id, - lv_plt_id = lv_plt_id, - my_qst_id = my_qst_id, - my_qst_sts = my_qst_sts - + def update_profile(self, profile_updated: Dict) -> None: + sql = profile.update(profile.c.user == profile_updated["user"]).values( + player_name=profile_updated["player_name"], + lv_num=profile_updated["lv_num"], + lv_pnt=profile_updated["lv_pnt"], + vcld_pts=profile_updated["vcld_pts"], + hp_vol=profile_updated["hp_vol"], + btn_se_vol=profile_updated["btn_se_vol"], + btn_se_vol2=profile_updated["btn_se_vol2"], + sldr_se_vol2=profile_updated["sldr_se_vol2"], + sort_kind=profile_updated["sort_kind"], + nxt_pv_id=profile_updated["nxt_pv_id"], + nxt_dffclty=profile_updated["nxt_dffclty"], + nxt_edtn=profile_updated["nxt_edtn"], + dsp_clr_brdr=profile_updated["dsp_clr_brdr"], + dsp_intrm_rnk=profile_updated["dsp_intrm_rnk"], + dsp_clr_sts=profile_updated["dsp_clr_sts"], + rgo_sts=profile_updated["rgo_sts"], + lv_efct_id=profile_updated["lv_efct_id"], + lv_plt_id=profile_updated["lv_plt_id"], + my_qst_id=profile_updated["my_qst_id"], + my_qst_sts=profile_updated["my_qst_sts"], + passwd_stat=profile_updated["passwd_stat"], + passwd=profile_updated["passwd"] ) result = self.execute(sql) - if result is None: - self.logger.error(f"update_profile: failed to update profile! profile: {profile_id}") + if result is None: + self.logger.error( + f"update_profile: failed to update profile! profile: {profile_id}") return None def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: @@ -104,10 +113,11 @@ class DivaProfileData(BaseData): 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 - )) - + 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.fetchone() diff --git a/titles/diva/schema/pv_customize.py b/titles/diva/schema/pv_customize.py new file mode 100644 index 0000000..e456e06 --- /dev/null +++ b/titles/diva/schema/pv_customize.py @@ -0,0 +1,69 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer, String +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +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("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("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' +) + + +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]: + + sql = insert(pv_customize).values( + version=version, + user=aime_id, + pv_id=pv_id, + mdl_eqp_ary=mdl_eqp_ary, + c_itm_eqp_ary=c_itm_eqp_ary, + ms_itm_flg_ary=ms_itm_flg_ary, + ) + + conflict = sql.on_duplicate_key_update( + pv_id=pv_id, + mdl_eqp_ary=mdl_eqp_ary, + c_itm_eqp_ary=c_itm_eqp_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 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]]: + """ + 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 + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index 6cd0346..638c25a 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -1,7 +1,7 @@ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func +from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert from typing import Optional, List, Dict, Any @@ -16,6 +16,7 @@ score = Table( Column("version", Integer), Column("pv_id", Integer), Column("difficulty", Integer), + Column("edition", Integer), Column("score", Integer), Column("atn_pnt", Integer), Column("clr_kind", Integer), @@ -26,7 +27,7 @@ score = Table( Column("sad", Integer), Column("worst", Integer), Column("max_combo", Integer), - UniqueConstraint("user", "pv_id", "difficulty", name="diva_score_uk"), + UniqueConstraint("user", "pv_id", "difficulty", "edition", name="diva_score_uk"), mysql_charset='utf8mb4' ) @@ -38,6 +39,7 @@ playlog = Table( Column("version", Integer), Column("pv_id", Integer), Column("difficulty", Integer), + Column("edition", Integer), Column("score", Integer), Column("atn_pnt", Integer), Column("clr_kind", Integer), @@ -52,90 +54,144 @@ playlog = Table( mysql_charset='utf8mb4' ) + class DivaScoreData(BaseData): - def put_best_score(self, user_id: int, game_version: int, song_id: int, difficulty: 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 """ sql = insert(score).values( user=user_id, version=game_version, - pv_id = song_id, + pv_id=song_id, difficulty=difficulty, + edition=edition, score=song_score, - atn_pnt = atn_pnt, - clr_kind = clr_kind, - sort_kind = sort_kind, - cool = cool, - fine = fine, - safe = safe, - sad = sad, - worst = worst, - max_combo = max_combo, + atn_pnt=atn_pnt, + clr_kind=clr_kind, + sort_kind=sort_kind, + cool=cool, + fine=fine, + safe=safe, + sad=sad, + worst=worst, + max_combo=max_combo, ) conflict = sql.on_duplicate_key_update( score=song_score, - atn_pnt = atn_pnt, - clr_kind = clr_kind, - sort_kind = sort_kind, - cool = cool, - fine = fine, - safe = safe, - sad = sad, - worst = worst, - max_combo = max_combo, + atn_pnt=atn_pnt, + clr_kind=clr_kind, + sort_kind=sort_kind, + cool=cool, + fine=fine, + safe=safe, + sad=sad, + worst=worst, + max_combo=max_combo, ) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}") + self.logger.error( + 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, 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 """ sql = playlog.insert().values( user=user_id, version=game_version, - pv_id = song_id, + pv_id=song_id, difficulty=difficulty, + edition=edition, score=song_score, - atn_pnt = atn_pnt, - clr_kind = clr_kind, - sort_kind = sort_kind, - cool = cool, - fine = fine, - safe = safe, - sad = sad, - worst = worst, - max_combo = max_combo + atn_pnt=atn_pnt, + clr_kind=clr_kind, + sort_kind=sort_kind, + cool=cool, + fine=fine, + safe=safe, + sad=sad, + worst=worst, + 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}") + self.logger.error( + f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}") return None - + return result.lastrowid - def get_best_score(self, user_id: int, pv_id: int, chart_id: 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 == chart_id) + and_(score.c.user == user_id, + score.c.pv_id == pv_id, + score.c.difficulty == difficulty, + score.c.edition == edition) ) 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]: + 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]: + # 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() + + # 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 + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_best_scores(self, user_id: int) -> Optional[List]: 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() diff --git a/titles/diva/schema/static.py b/titles/diva/schema/static.py index 6544420..c8d83bd 100644 --- a/titles/diva/schema/static.py +++ b/titles/diva/schema/static.py @@ -132,11 +132,25 @@ class DivaStaticData(BaseData): if result is None: return None return result.lastrowid - def get_enabled_shop(self, version: int) -> Optional[List[Row]]: - sql = select(shop).where(and_(shop.c.version == version, shop.c.enabled == True)) + 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)) result = self.execute(sql) - if result is None: return None + if result is None: + return None + 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)) + + 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]: @@ -158,12 +172,26 @@ class DivaStaticData(BaseData): result = self.execute(conflict) if result is None: return None return result.lastrowid - - def get_enabled_items(self, version: int) -> Optional[List[Row]]: - sql = select(items).where(and_(items.c.version == version, items.c.enabled == True)) + + 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)) result = self.execute(sql) - if result is None: return None + if result is None: + return None + 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)) + + 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, From edddb2e9d4260687ab9c0b8dc67818c5814f7567 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 22:58:40 -0500 Subject: [PATCH 002/589] fix allnet request using the wrong function --- core/allnet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 3b371ce..7e5accd 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -151,7 +151,7 @@ class AllnetServlet: def handle_dlorder(self, request: Request): request_ip = request.getClientAddress().host try: - req = AllnetDownloadOrderRequest(self.allnet_req_to_dict(request.content.getvalue())) + req = AllnetDownloadOrderRequest(self.billing_req_to_dict(request.content.getvalue())) # 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: @@ -243,7 +243,7 @@ class AllnetServlet: ret.append(tmp) - def allnet_req_to_dict(self, data: bytes): + def billing_req_to_dict(self, data: bytes): """ Parses an billing request string into a python dictionary """ @@ -258,7 +258,7 @@ class AllnetServlet: print(e) return None - def billing_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: + def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: """ Parses an allnet request string into a python dictionary """ From 9a43303880b507a5c518555d392ae7814aef6309 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:02:50 -0500 Subject: [PATCH 003/589] fix incorrect class being used --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 7e5accd..9e69c0e 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -252,7 +252,7 @@ class AllnetServlet: unzipped = decomp.decompress(data) sections = unzipped.decode('ascii').split('\r\n') - return Utils.kvp_to_dict(sections) + return self.kvp_to_dict(sections) except Exception as e: print(e) @@ -267,7 +267,7 @@ class AllnetServlet: unzipped = zlib.decompress(zipped) sections = unzipped.decode('utf-8').split('\r\n') - return Utils.kvp_to_dict(sections) + return self.kvp_to_dict(sections) except Exception as e: print(e) From 655d9dc5308da72bca1d8bb4415583ecd5ec3199 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:12:40 -0500 Subject: [PATCH 004/589] simplified main dispatcher --- core/allnet.py | 6 +++--- core/mucha.py | 4 ++-- index.py | 36 +++++++++--------------------------- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 9e69c0e..de0d20c 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -92,7 +92,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}") - def handle_poweron(self, request: Request): + def handle_poweron(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())) @@ -148,7 +148,7 @@ class AllnetServlet: return self.dict_to_http_form_string([vars(resp)]) - def handle_dlorder(self, request: Request): + def handle_dlorder(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: req = AllnetDownloadOrderRequest(self.billing_req_to_dict(request.content.getvalue())) @@ -168,7 +168,7 @@ class AllnetServlet: else: # TODO: Actual dlorder response return self.dict_to_http_form_string(vars(resp)) - def handle_billing_request(self, request: Request): + def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) request_ip = request.getClientAddress() if req_dict is None: diff --git a/core/mucha.py b/core/mucha.py index a861101..312d83f 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -28,7 +28,7 @@ class MuchaServlet: self.logger.setLevel(logging.INFO) coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) - def handle_boardauth(self, request: Request) -> bytes: + 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()}") @@ -41,7 +41,7 @@ class MuchaServlet: return self.mucha_postprocess(vars(resp)) - def handle_updatecheck(self, request: Request) -> bytes: + def handle_updatecheck(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) if req_dict is None: self.logger.error(f"Error processing mucha request {request.content.getvalue()}") diff --git a/index.py b/index.py index 7fae586..0049427 100644 --- a/index.py +++ b/index.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +from typing import Dict import yaml from os import path, mkdir, access, W_OK from core import * @@ -36,46 +37,27 @@ class HttpDispatcher(resource.Resource): if test is None: return b"" - controller = getattr(self, test["controller"], None) - if controller is None: - return b"" - - handler = getattr(controller, test["action"], None) - if handler is None: - return b"" - - url_vars = test - url_vars.pop("controller") - url_vars.pop("action") - - if len(url_vars) > 0: - ret = handler(request, url_vars) - else: - ret = handler(request) - - if type(ret) == str: - return ret.encode() - elif type(ret) == bytes: - return ret - else: - return b"" + return self.dispatch(test, request) def render_POST(self, request: Request) -> bytes: test = self.map_post.match(request.uri.decode()) if test is None: return b"" + + return self.dispatch(test, request) - controller = getattr(self, test["controller"], None) + def dispatch(self, matcher: Dict, request: Request) -> bytes: + controller = getattr(self, matcher["controller"], None) if controller is None: return b"" - handler = getattr(controller, test["action"], None) + handler = getattr(controller, matcher["action"], None) if handler is None: return b"" - url_vars = test + url_vars = matcher url_vars.pop("controller") - url_vars.pop("action") + url_vars.pop("action") ret = handler(request, url_vars) if type(ret) == str: From 68b9c64f7147d56839d3756bbd8f66413b905334 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:24:38 -0500 Subject: [PATCH 005/589] fix kvp_to_dict --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index de0d20c..939e16e 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -230,7 +230,7 @@ class AllnetServlet: self.logger.debug(f"response {vars(resp)}") return resp_str.encode("utf-8") - def kvp_to_dict(self, *kvp: str) -> List[Dict[str, Any]]: + def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]: ret: List[Dict[str, Any]] = [] for x in kvp: items = x.split('&') From b09d2326c26db762cbdfd0499551c527ec9a602b Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:27:25 -0500 Subject: [PATCH 006/589] fix kvp_to_dict not returning --- core/allnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 939e16e..95de25b 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -242,6 +242,8 @@ class AllnetServlet: tmp[kvp[0]] = kvp[1] ret.append(tmp) + + return ret def billing_req_to_dict(self, data: bytes): """ From f42c2d7785fe0f0f5178e3d784b610533f45f0b6 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:31:52 -0500 Subject: [PATCH 007/589] fix allnet requests not processing --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 95de25b..49b7828 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -95,7 +95,7 @@ class AllnetServlet: def handle_poweron(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: - req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())) + req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())[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: @@ -151,7 +151,7 @@ class AllnetServlet: def handle_dlorder(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: - req = AllnetDownloadOrderRequest(self.billing_req_to_dict(request.content.getvalue())) + req = AllnetDownloadOrderRequest(self.billing_req_to_dict(request.content.getvalue())[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: From a843e3d3ac1e67f66fc483272a77c939f40cba95 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:40:19 -0500 Subject: [PATCH 008/589] added token replacement --- core/allnet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 49b7828..b528295 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -142,11 +142,15 @@ class AllnetServlet: req.region_name2 = arcade["city"] req.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" + int_ver = req.ver.replace(".", "") + req.uri = req.uri.replace("$v", int_ver) + req.host = req.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) - return self.dict_to_http_form_string([vars(resp)]) + return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") def handle_dlorder(self, request: Request, _: Dict): request_ip = request.getClientAddress().host From a3c689cd0931e01ae99f2b63b947a58dea85e383 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:42:27 -0500 Subject: [PATCH 009/589] fix using the request instead of the response --- core/allnet.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index b528295..9d59c2e 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -131,20 +131,20 @@ class AllnetServlet: if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - req.country = arcade["country"] if machine["country"] is None else machine["country"] - req.place_id = arcade["id"] - req.allnet_id = machine["id"] - req.name = arcade["name"] - req.nickname = arcade["nickname"] - req.region0 = arcade["region_id"] - req.region_name0 = arcade["country"] - req.region_name1 = arcade["state"] - req.region_name2 = arcade["city"] - req.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" + resp.country = arcade["country"] if machine["country"] is None else machine["country"] + resp.place_id = arcade["id"] + resp.allnet_id = machine["id"] + resp.name = arcade["name"] + resp.nickname = arcade["nickname"] + resp.region0 = arcade["region_id"] + resp.region_name0 = arcade["country"] + resp.region_name1 = arcade["state"] + resp.region_name2 = arcade["city"] + resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" int_ver = req.ver.replace(".", "") - req.uri = req.uri.replace("$v", int_ver) - req.host = req.host.replace("$v", int_ver) + 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) @@ -227,7 +227,7 @@ class AllnetServlet: resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) - resp_str = self.dict_to_http_form_string([vars(resp)]) + resp_str = self.dict_to_http_form_string([vars(resp)], True) if resp_str is None: self.logger.error(f"Failed to parse response {vars(resp)}") From ed479866cc8fbc6966f9e5c424a03fd6beffc65e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 18 Feb 2023 23:57:45 -0500 Subject: [PATCH 010/589] fix download order --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 9d59c2e..b32b7f7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -155,7 +155,7 @@ class AllnetServlet: def handle_dlorder(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: - req = AllnetDownloadOrderRequest(self.billing_req_to_dict(request.content.getvalue())[0]) + req = AllnetDownloadOrderRequest(self.allnet_req_to_dict(request.content.getvalue())[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: From f5d9bd8003cb07ae3ad72188f8700951730b2c48 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 00:01:39 -0500 Subject: [PATCH 011/589] fix copypasta code --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index b32b7f7..9aea93e 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -158,8 +158,8 @@ class AllnetServlet: req = AllnetDownloadOrderRequest(self.allnet_req_to_dict(request.content.getvalue())[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip: - raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}") + if not req.game_id or not req.ver or not req.serial: + raise AllnetRequestException(f"Bad download request params from {request_ip} - {vars(req)}") except AllnetRequestException as e: self.logger.error(e) From d434bf084d95e76c90b45bd51a3a145abdcbc46b Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 00:06:21 -0500 Subject: [PATCH 012/589] fix sending incorrect params to dict_to_http_form_string --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 9aea93e..08f636a 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -167,10 +167,10 @@ class AllnetServlet: resp = AllnetDownloadOrderResponse() if not self.config.allnet.allow_online_updates: - return self.dict_to_http_form_string(vars(resp)) + return self.dict_to_http_form_string([vars(resp)]) else: # TODO: Actual dlorder response - return self.dict_to_http_form_string(vars(resp)) + return self.dict_to_http_form_string([vars(resp)]) def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) From a57d2cf71c04cc9b5efb943db1b44ea49b956ef4 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 00:07:14 -0500 Subject: [PATCH 013/589] fix typo in aimedb --- core/aimedb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/aimedb.py b/core/aimedb.py index b8ce3b7..78ce350 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -164,7 +164,7 @@ class AimedbProtocol(Protocol): def handle_register(self, data: bytes) -> bytes: luid = data[0x20: 0x2a].hex() - if self.config.server.allow_registration: + if self.config.server.allow_user_registration: user_id = self.data.user.create_user() if user_id is None: From df4efa1fda20f0c2a64dbbbf188f57cdb09153ac Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 00:10:42 -0500 Subject: [PATCH 014/589] replace print statements with error logging --- core/allnet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 08f636a..bd2ecce 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -261,7 +261,7 @@ class AllnetServlet: return self.kvp_to_dict(sections) except Exception as e: - print(e) + self.logger.error(e) return None def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: @@ -276,7 +276,7 @@ class AllnetServlet: return self.kvp_to_dict(sections) except Exception as e: - print(e) + self.logger.error(e) return None def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]: @@ -303,7 +303,7 @@ class AllnetServlet: return urlencode except Exception as e: - print(e) + self.logger.error(e) return None class AllnetPowerOnRequest(): From 8bdc2071dab2941fb5feb821832758214377378e Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 19 Feb 2023 16:08:53 +0100 Subject: [PATCH 015/589] diva: changed player_name length to 10 --- core/data/schema/versions/SBZV_3_rollback.sql | 1 + core/data/schema/versions/SBZV_4_upgrade.sql | 1 + titles/diva/schema/profile.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/data/schema/versions/SBZV_3_rollback.sql b/core/data/schema/versions/SBZV_3_rollback.sql index 00d95e1..18c2448 100644 --- a/core/data/schema/versions/SBZV_3_rollback.sql +++ b/core/data/schema/versions/SBZV_3_rollback.sql @@ -1,2 +1,3 @@ ALTER TABLE diva_profile DROP COLUMN passwd_stat; ALTER TABLE diva_profile DROP COLUMN passwd; +ALTER TABLE diva_profile MODIFY player_name VARCHAR(8); diff --git a/core/data/schema/versions/SBZV_4_upgrade.sql b/core/data/schema/versions/SBZV_4_upgrade.sql index d3aacb6..c3a802c 100644 --- a/core/data/schema/versions/SBZV_4_upgrade.sql +++ b/core/data/schema/versions/SBZV_4_upgrade.sql @@ -1,2 +1,3 @@ ALTER TABLE diva_profile ADD COLUMN passwd_stat INTEGER NOT NULL DEFAULT 0; ALTER TABLE diva_profile ADD COLUMN passwd VARCHAR(12) NOT NULL DEFAULT "**********"; +ALTER TABLE diva_profile MODIFY player_name VARCHAR(10); diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 76e8241..272026c 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -14,7 +14,7 @@ profile = Table( Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column("version", Integer, nullable=False), - Column("player_name", String(8), nullable=False), + Column("player_name", String(10), nullable=False), Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"), Column("lv_num", Integer, nullable=False, server_default="0"), Column("lv_pnt", Integer, nullable=False, server_default="0"), From 97d16365df1708996ebcbbf49ea49ed0922bc86f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 14:52:20 -0500 Subject: [PATCH 016/589] carry over database functions from megaime --- core/data/database.py | 1703 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1702 insertions(+), 1 deletion(-) diff --git a/core/data/database.py b/core/data/database.py index 800b5d0..963d016 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -4,11 +4,14 @@ 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 from hashlib import sha256 from core.config import CoreConfig from core.data.schema import * +from core.utils import Utils class Data: def __init__(self, cfg: CoreConfig) -> None: @@ -50,4 +53,1702 @@ class Data: coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str) self.logger.handler_set = True # type: ignore - \ No newline at end of file + def create_database(self): + self.logger.info("Creating databases...") + try: + metadata.create_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to create databases! {e}") + return + + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + try: + 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]) + + except Exception as e: + self.logger.warning(f"Could not load database schema from {game_dir} - {e}") + + self.logger.info(f"Setting base_schema_ver to {self.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) + + def recreate_database(self): + self.logger.info("Dropping all databases...") + self.base.execute("SET FOREIGN_KEY_CHECKS=0") + try: + metadata.drop_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to drop databases! {e}") + return + + for root, dirs, files in os.walk("./titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") + + try: + 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}") + + except ImportError as e: + self.logger.warning(f"Failed to load database schema dir {dir} - {e}") + break + + self.base.execute("SET FOREIGN_KEY_CHECKS=1") + + self.create_database() + + def migrate_database(self, game: str, version: 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") + 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 dump_db(self): + dbname = self.config.database.name + + self.logger.info("Database dumper for use with the reworked schema") + self.logger.info("Dumping users...") + + sql = f"SELECT * FROM `{dbname}`.`user`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + users = result.fetchall() + + user_list: List[Dict[str, Any]] = [] + for usr in users: + user_list.append({ + "id": usr["id"], + "username": usr["username"], + "email": usr["email"], + "password": usr["password"], + "permissions": usr["permissions"], + "created_date": datetime.strftime(usr["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(usr["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(user_list)} users") + with open("dbdump-user.json", "w", encoding="utf-8") as f: + f.write(json.dumps(user_list)) + self.logger.info(f"Saved as dbdump-user.json") + + self.logger.info("Dumping cards...") + + sql = f"SELECT * FROM `{dbname}`.`card`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + cards = result.fetchall() + + card_list: List[Dict[str, Any]] = [] + for crd in cards: + card_list.append({ + "id": crd["id"], + "user": crd["user"], + "access_code": crd["access_code"], + "is_locked": crd["is_locked"], + "is_banned": crd["is_banned"], + "created_date": datetime.strftime(crd["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(crd["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(card_list)} cards") + with open("dbdump-card.json", "w", encoding="utf-8") as f: + f.write(json.dumps(card_list)) + self.logger.info(f"Saved as dbdump-card.json") + + self.logger.info("Dumping arcades...") + + sql = f"SELECT * FROM `{dbname}`.`arcade`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcades = result.fetchall() + + arcade_list: List[Dict[str, Any]] = [] + for arc in arcades: + arcade_list.append({ + "id": arc["id"], + "name": arc["name"], + "nickname": arc["name"], + "country": None, + "country_id": None, + "state": None, + "city": None, + "region_id": None, + "timezone": None, + }) + + self.logger.info(f"Done, found {len(arcade_list)} arcades") + with open("dbdump-arcade.json", "w", encoding="utf-8") as f: + f.write(json.dumps(arcade_list)) + self.logger.info(f"Saved as dbdump-arcade.json") + + self.logger.info("Dumping machines...") + + sql = f"SELECT * FROM `{dbname}`.`machine`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + machines = result.fetchall() + + machine_list: List[Dict[str, Any]] = [] + for mech in machines: + if "country" in mech["data"]: + country = mech["data"]["country"] + else: + country = None + + if "ota_enable" in mech["data"]: + ota_enable = mech["data"]["ota_enable"] + else: + ota_enable = None + + machine_list.append({ + "id": mech["id"], + "arcade": mech["arcade"], + "serial": mech["keychip"], + "game": mech["game"], + "board": None, + "country": country, + "timezone": None, + "ota_enable": ota_enable, + "is_cab": False, + }) + + self.logger.info(f"Done, found {len(machine_list)} machines") + with open("dbdump-machine.json", "w", encoding="utf-8") as f: + f.write(json.dumps(machine_list)) + self.logger.info(f"Saved as dbdump-machine.json") + + self.logger.info("Dumping arcade owners...") + + sql = f"SELECT * FROM `{dbname}`.`arcade_owner`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcade_owners = result.fetchall() + + owner_list: List[Dict[str, Any]] = [] + for owner in owner_list: + owner_list.append(owner._asdict()) + + self.logger.info(f"Done, found {len(owner_list)} arcade owners") + with open("dbdump-arcade_owner.json", "w", encoding="utf-8") as f: + f.write(json.dumps(owner_list)) + self.logger.info(f"Saved as dbdump-arcade_owner.json") + + self.logger.info("Dumping profiles...") + + sql = f"SELECT * FROM `{dbname}`.`profile`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + profiles = result.fetchall() + + profile_list: Dict[List[Dict[str, Any]]] = {} + for pf in profiles: + game = pf["game"] + + if game not in profile_list: + profile_list[game] = [] + + profile_list[game].append({ + "id": pf["id"], + "user": pf["user"], + "version": pf["version"], + "use_count": pf["use_count"], + "name": pf["name"], + "game_id": pf["game_id"], + "mods": pf["mods"], + "data": pf["data"], + }) + + self.logger.info(f"Done, found profiles for {len(profile_list)} games") + with open("dbdump-profile.json", "w", encoding="utf-8") as f: + f.write(json.dumps(profile_list)) + self.logger.info(f"Saved as dbdump-profile.json") + + self.logger.info("Dumping scores...") + + sql = f"SELECT * FROM `{dbname}`.`score`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + scores = result.fetchall() + + score_list: Dict[List[Dict[str, Any]]] = {} + for sc in scores: + game = sc["game"] + + if game not in score_list: + score_list[game] = [] + + score_list[game].append({ + "id": sc["id"], + "user": sc["user"], + "version": sc["version"], + "song_id": sc["song_id"], + "chart_id": sc["chart_id"], + "score1": sc["score1"], + "score2": sc["score2"], + "fc1": sc["fc1"], + "fc2": sc["fc2"], + "cleared": sc["cleared"], + "grade": sc["grade"], + "data": sc["data"], + }) + + self.logger.info(f"Done, found scores for {len(score_list)} games") + with open("dbdump-score.json", "w", encoding="utf-8") as f: + f.write(json.dumps(score_list)) + self.logger.info(f"Saved as dbdump-score.json") + + self.logger.info("Dumping achievements...") + + sql = f"SELECT * FROM `{dbname}`.`achievement`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + achievements = result.fetchall() + + achievement_list: Dict[List[Dict[str, Any]]] = {} + for ach in achievements: + game = ach["game"] + + if game not in achievement_list: + achievement_list[game] = [] + + achievement_list[game].append({ + "id": ach["id"], + "user": ach["user"], + "version": ach["version"], + "type": ach["type"], + "achievement_id": ach["achievement_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found achievements for {len(achievement_list)} games") + with open("dbdump-achievement.json", "w", encoding="utf-8") as f: + f.write(json.dumps(achievement_list)) + self.logger.info(f"Saved as dbdump-achievement.json") + + self.logger.info("Dumping items...") + + sql = f"SELECT * FROM `{dbname}`.`item`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + items = result.fetchall() + + item_list: Dict[List[Dict[str, Any]]] = {} + for itm in items: + game = itm["game"] + + if game not in item_list: + item_list[game] = [] + + item_list[game].append({ + "id": itm["id"], + "user": itm["user"], + "version": itm["version"], + "type": itm["type"], + "item_id": itm["item_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found items for {len(item_list)} games") + with open("dbdump-item.json", "w", encoding="utf-8") as f: + f.write(json.dumps(item_list)) + self.logger.info(f"Saved as dbdump-item.json") + + def restore_from_old_schema(self): + # Import the tables we expect to be there + from core.data.schema.user import aime_user + from core.data.schema.card import aime_card + from core.data.schema.arcade import arcade, machine, arcade_owner + from sqlalchemy.dialects.mysql import Insert + + # Make sure that all the tables we're trying to access exist + self.create_database() + + # Import the data, making sure that dependencies are accounted for + if os.path.exists("dbdump-user.json"): + users = [] + with open("dbdump-user.json", "r", encoding="utf-8") as f: + users = json.load(f) + + self.logger.info(f"Load {len(users)} users") + + for user in users: + sql = Insert(aime_user).values(**user) + + conflict = sql.on_duplicate_key_update(**user) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert user {user['id']}") + continue + self.logger.info(f"Inserted user {user['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-card.json"): + cards = [] + with open("dbdump-card.json", "r", encoding="utf-8") as f: + cards = json.load(f) + + self.logger.info(f"Load {len(cards)} cards") + + for card in cards: + sql = Insert(aime_card).values(**card) + + conflict = sql.on_duplicate_key_update(**card) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert card {card['id']}") + continue + self.logger.info(f"Inserted card {card['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade.json"): + arcades = [] + with open("dbdump-arcade.json", "r", encoding="utf-8") as f: + arcades = json.load(f) + + self.logger.info(f"Load {len(arcades)} arcades") + + for ac in arcades: + sql = Insert(arcade).values(**ac) + + conflict = sql.on_duplicate_key_update(**ac) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade {ac['id']}") + continue + self.logger.info(f"Inserted arcade {ac['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade_owner.json"): + ac_owners = [] + with open("dbdump-arcade_owner.json", "r", encoding="utf-8") as f: + ac_owners = json.load(f) + + self.logger.info(f"Load {len(ac_owners)} arcade owners") + + for owner in ac_owners: + sql = Insert(arcade_owner).values(**owner) + + conflict = sql.on_duplicate_key_update(**owner) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade_owner {owner['user']}") + continue + self.logger.info(f"Inserted arcade_owner {owner['user']} -> {result.lastrowid}") + + if os.path.exists("dbdump-machine.json"): + mechs = [] + with open("dbdump-machine.json", "r", encoding="utf-8") as f: + mechs = json.load(f) + + self.logger.info(f"Load {len(mechs)} machines") + + for mech in mechs: + sql = Insert(machine).values(**mech) + + conflict = sql.on_duplicate_key_update(**mech) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert machine {mech['id']}") + continue + self.logger.info(f"Inserted machine {mech['id']} -> {result.lastrowid}") + + # Now the fun part, grabbing all our scores, profiles, items, and achievements and trying + # to conform them to our current, freeform schema. This will be painful... + profiles = {} + items = {} + scores = {} + achievements = {} + + if os.path.exists("dbdump-profile.json"): + with open("dbdump-profile.json", "r", encoding="utf-8") as f: + profiles = json.load(f) + + self.logger.info(f"Load {len(profiles)} profiles") + + if os.path.exists("dbdump-item.json"): + with open("dbdump-item.json", "r", encoding="utf-8") as f: + items = json.load(f) + + self.logger.info(f"Load {len(items)} items") + + if os.path.exists("dbdump-score.json"): + with open("dbdump-score.json", "r", encoding="utf-8") as f: + scores = json.load(f) + + self.logger.info(f"Load {len(scores)} scores") + + if os.path.exists("dbdump-achievement.json"): + with open("dbdump-achievement.json", "r", encoding="utf-8") as f: + achievements = json.load(f) + + self.logger.info(f"Load {len(achievements)} achievements") + + # Chuni / Chusan + if os.path.exists("titles/chuni/schema"): + from titles.chuni.schema.item import character, item, duel, map, map_area + from titles.chuni.schema.profile import profile, profile_ex, option, option_ex + from titles.chuni.schema.profile import recent_rating, activity, charge, emoney + from titles.chuni.schema.profile import overpower + from titles.chuni.schema.score import best_score, course + + chuni_profiles = [] + chuni_items = [] + chuni_scores = [] + + if "SDBT" in profiles: + chuni_profiles = profiles["SDBT"] + if "SDBT" in items: + chuni_items = items["SDBT"] + if "SDBT" in scores: + chuni_scores = scores["SDBT"] + if "SDHD" in profiles: + chuni_profiles += profiles["SDHD"] + if "SDHD" in items: + chuni_items += items["SDHD"] + if "SDHD" in scores: + chuni_scores += scores["SDHD"] + + self.logger.info(f"Importing {len(chuni_profiles)} chunithm/chunithm new profiles") + + for pf in chuni_profiles: + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + pf_data = pf["data"] + + # data + if "userData" in pf_data: + pf_data["userData"]["userName"] = bytes([ord(c) for c in pf_data["userData"]["userName"]]).decode("utf-8") + pf_data["userData"]["user"] = pf["user"] + pf_data["userData"]["version"] = pf["version"] + pf_data["userData"].pop("accessCode") + + if pf_data["userData"]["lastRomVersion"].startswith("2."): + pf_data["userData"]["version"] += 10 + + pf_data["userData"] = self.base.fix_bools(pf_data["userData"]) + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile for {pf['user']} ->{result.lastrowid}") + + # data_ex + if "userDataEx" in pf_data and len(pf_data["userDataEx"]) > 0: + pf_data["userDataEx"][0]["user"] = pf["user"] + pf_data["userDataEx"][0]["version"] = pf["version"] + + pf_data["userDataEx"] = self.base.fix_bools(pf_data["userDataEx"][0]) + + sql = Insert(profile_ex).values(**pf_data["userDataEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userDataEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile data_ex for {pf['user']} ->{result.lastrowid}") + + # option + if "userGameOption" in pf_data: + pf_data["userGameOption"]["user"] = pf["user"] + + pf_data["userGameOption"] = self.base.fix_bools(pf_data["userGameOption"]) + + sql = Insert(option).values(**pf_data["userGameOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOption"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile options for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile options for {pf['user']} ->{result.lastrowid}") + + # option_ex + if "userGameOptionEx" in pf_data and len(pf_data["userGameOptionEx"]) > 0: + pf_data["userGameOptionEx"][0]["user"] = pf["user"] + + pf_data["userGameOptionEx"] = self.base.fix_bools(pf_data["userGameOptionEx"][0]) + + sql = Insert(option_ex).values(**pf_data["userGameOptionEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOptionEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile option_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile option_ex for {pf['user']} ->{result.lastrowid}") + + # recent_rating + if "userRecentRatingList" in pf_data: + rr = { + "user": pf["user"], + "recentRating": pf_data["userRecentRatingList"] + } + + sql = Insert(recent_rating).values(**rr) + conflict = sql.on_duplicate_key_update(**rr) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile recent_rating for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile recent_rating for {pf['user']} ->{result.lastrowid}") + + # activity + if "userActivityList" in pf_data: + for act in pf_data["userActivityList"]: + act["user"] = pf["user"] + + sql = Insert(activity).values(**act) + conflict = sql.on_duplicate_key_update(**act) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile activity for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile activity for {pf['user']} ->{result.lastrowid}") + + # charge + if "userChargeList" in pf_data: + for cg in pf_data["userChargeList"]: + cg["user"] = pf["user"] + + cg = self.base.fix_bools(cg) + + sql = Insert(charge).values(**cg) + conflict = sql.on_duplicate_key_update(**cg) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile charge for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile charge for {pf['user']} ->{result.lastrowid}") + + # emoney + if "userEmoneyList" in pf_data: + for emon in pf_data["userEmoneyList"]: + emon["user"] = pf["user"] + + sql = Insert(emoney).values(**emon) + conflict = sql.on_duplicate_key_update(**emon) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile emoney for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile emoney for {pf['user']} ->{result.lastrowid}") + + # overpower + if "userOverPowerList" in pf_data: + for op in pf_data["userOverPowerList"]: + op["user"] = pf["user"] + + sql = Insert(overpower).values(**op) + conflict = sql.on_duplicate_key_update(**op) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile overpower for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile overpower for {pf['user']} ->{result.lastrowid}") + + # map_area + if "userMapAreaList" in pf_data: + for ma in pf_data["userMapAreaList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map_area).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map_area for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map_area for {pf['user']} ->{result.lastrowid}") + + #duel + if "userDuelList" in pf_data: + for ma in pf_data["userDuelList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(duel).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni duel for {pf['user']}") + else: + self.logger.info(f"Inserted chuni duel for {pf['user']} ->{result.lastrowid}") + + # map + if "userMapList" in pf_data: + for ma in pf_data["userMapList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map for {pf['user']} ->{result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_items)} chunithm/chunithm new items") + + for i in chuni_items: + if type(i["data"]) is not dict: + i["data"] = json.loads(i["data"]) + i_data = i["data"] + + i_data["user"] = i["user"] + + i_data = self.base.fix_bools(i_data) + + try: i_data.pop("assignIllust") + except: pass + + try: i_data.pop("exMaxLv") + except: pass + + if i["type"] == 20: #character + sql = Insert(character).values(**i_data) + else: + sql = Insert(item).values(**i_data) + + conflict = sql.on_duplicate_key_update(**i_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni item for user {i['user']}") + + else: + self.logger.info(f"Inserted chuni item for user {i['user']} {i['item_id']} -> {result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_scores)} chunithm/chunithm new scores") + + for sc in chuni_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + score_data = self.base.fix_bools(sc["data"]) + + try: score_data.pop("theoryCount") + except: pass + + try: score_data.pop("ext1") + except: pass + + score_data["user"] = sc["user"] + + sql = Insert(best_score).values(**score_data) + conflict = sql.on_duplicate_key_update(**score_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to put chuni score for user {sc['user']}") + else: + self.logger.info(f"Inserted chuni score for user {sc['user']} {sc['song_id']}/{sc['chart_id']} -> {result.lastrowid}") + + else: + self.logger.info(f"Chuni/Chusan not found, skipping...") + + # CXB + if os.path.exists("titles/cxb/schema"): + from titles.cxb.schema.item import energy + from titles.cxb.schema.profile import profile + from titles.cxb.schema.score import score, ranking + + cxb_profiles = [] + cxb_items = [] + cxb_scores = [] + + if "SDCA" in profiles: + cxb_profiles = profiles["SDCA"] + if "SDCA" in items: + cxb_items = items["SDCA"] + if "SDCA" in scores: + cxb_scores = scores["SDCA"] + + self.logger.info(f"Importing {len(cxb_profiles)} CXB profiles") + + for pf in cxb_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"]["data"] + pf_idx = pf["data"]["index"] + + for x in range(len(pf_data)): + sql = Insert(profile).values( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB profile for user {user} Index {pf_idx[x]}") + + self.logger.info(f"Importing {len(cxb_scores)} CXB scores") + + for sc in cxb_scores: + user = sc["user"] + version = sc["version"] + mcode = sc["data"]["mcode"] + index = sc["data"]["index"] + + sql = Insert(score).values( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB score for user {user} mcode {mcode}") + + self.logger.info(f"Importing {len(cxb_items)} CXB items") + + for it in cxb_items: + user = it["user"] + + if it["type"] == 3: # energy + sql = Insert(energy).values( + user = user, + energy = it["data"]["total"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + energy = it["data"]["total"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB energy for user {user}") + + elif it["type"] == 2: + sql = Insert(ranking).values( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + conflict = sql.on_duplicate_key_update( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB ranking for user {user}") + + else: + self.logger.error(f"Unknown CXB item type {it['type']} for user {user}") + + else: + self.logger.info(f"CXB not found, skipping...") + + # Diva + if os.path.exists("titles/diva/schema"): + from titles.diva.schema.profile import profile + from titles.diva.schema.score import score + from titles.diva.schema.item import shop + + diva_profiles = [] + diva_scores = [] + + if "SBZV" in profiles: + diva_profiles = profiles["SBZV"] + if "SBZV" in scores: + diva_scores = scores["SBZV"] + + self.logger.info(f"Importing {len(diva_profiles)} Diva profiles") + + for pf in diva_profiles: + pf["data"]["user"] = pf["user"] + pf["data"]["version"] = pf["version"] + pf_data = pf["data"] + + if "mdl_eqp_ary" in pf["data"]: + sql = Insert(shop).values( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"], + ) + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"] + ) + self.base.execute(conflict) + pf["data"].pop("mdl_eqp_ary") + + sql = Insert(profile).values(**pf_data) + conflict = sql.on_duplicate_key_update(**pf_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva profile for {pf['user']}") + + self.logger.info(f"Importing {len(diva_scores)} Diva scores") + + for sc in diva_scores: + user = sc["user"] + + clr_kind = -1 + for x in sc["data"]["stg_clr_kind"].split(","): + if x != "-1": + clr_kind = x + + cool_ct = 0 + for x in sc["data"]["stg_cool_cnt"].split(","): + if x != "0": + cool_ct = x + + fine_ct = 0 + for x in sc["data"]["stg_fine_cnt"].split(","): + if x != "0": + fine_ct = x + + safe_ct = 0 + for x in sc["data"]["stg_safe_cnt"].split(","): + if x != "0": + safe_ct = x + + sad_ct = 0 + for x in sc["data"]["stg_sad_cnt"].split(","): + if x != "0": + sad_ct = x + + worst_ct = 0 + for x in sc["data"]["stg_wt_wg_cnt"].split(","): + if x != "0": + worst_ct = x + + max_cmb = 0 + for x in sc["data"]["stg_max_cmb"].split(","): + if x != "0": + max_cmb = x + + sql = Insert(score).values( + user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb, + ) + + conflict = sql.on_duplicate_key_update(user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva score for {pf['user']}") + + else: + self.logger.info(f"Diva not found, skipping...") + + # Ongeki + if os.path.exists("titles/ongeki/schema"): + from titles.ongeki.schema.item import card, deck, character, boss, story + from titles.ongeki.schema.item import chapter, item, music_item, login_bonus + from titles.ongeki.schema.item import event_point, mission_point, scenerio + from titles.ongeki.schema.item import trade_item, event_music, tech_event + from titles.ongeki.schema.profile import profile, option, activity, recent_rating + from titles.ongeki.schema.profile import rating_log, training_room, kop + from titles.ongeki.schema.score import score_best, tech_count, playlog + from titles.ongeki.schema.log import session_log + + item_types = { + "character": 20, + "story": 21, + "card": 22, + "deck": 23, + "login": 24, + "chapter": 25 + } + + ongeki_profiles = [] + ongeki_items = [] + ongeki_scores = [] + + if "SDDT" in profiles: + ongeki_profiles = profiles["SDDT"] + if "SDDT" in items: + ongeki_items = items["SDDT"] + if "SDDT" in scores: + ongeki_scores = scores["SDDT"] + + self.logger.info(f"Importing {len(ongeki_profiles)} ongeki profiles") + + for pf in ongeki_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"] + + pf_data["userData"]["user"] = user + pf_data["userData"]["version"] = version + pf_data["userData"].pop("accessCode") + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile data for user {pf['user']}") + continue + + pf_data["userOption"]["user"] = user + + sql = Insert(option).values(**pf_data["userOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userOption"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile options for user {pf['user']}") + continue + + for pf_list in pf_data["userActivityList"]: + pf_list["user"] = user + + sql = Insert(activity).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile activity for user {pf['user']}") + continue + + sql = Insert(recent_rating).values( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + result = self.base.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert ongeki profile recent rating for user {pf['user']}") + continue + + for pf_list in pf_data["userRatinglogList"]: + pf_list["user"] = user + + sql = Insert(rating_log).values(**pf_list) + + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile rating log for user {pf['user']}") + continue + + for pf_list in pf_data["userTrainingRoomList"]: + pf_list["user"] = user + + sql = Insert(training_room).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + if "userKopList" in pf_data: + for pf_list in pf_data["userKopList"]: + pf_list["user"] = user + + sql = Insert(kop).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + for pf_list in pf_data["userBossList"]: + pf_list["user"] = user + + sql = Insert(boss).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item boss for user {pf['user']}") + continue + + for pf_list in pf_data["userDeckList"]: + pf_list["user"] = user + + sql = Insert(deck).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item deck for user {pf['user']}") + continue + + for pf_list in pf_data["userStoryList"]: + pf_list["user"] = user + + sql = Insert(story).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item story for user {pf['user']}") + continue + + for pf_list in pf_data["userChapterList"]: + pf_list["user"] = user + + sql = Insert(chapter).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item chapter for user {pf['user']}") + continue + + for pf_list in pf_data["userPlaylogList"]: + pf_list["user"] = user + + sql = Insert(playlog).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score playlog for user {pf['user']}") + continue + + for pf_list in pf_data["userMusicItemList"]: + pf_list["user"] = user + + sql = Insert(music_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item music item for user {pf['user']}") + continue + + for pf_list in pf_data["userTechCountList"]: + pf_list["user"] = user + + sql = Insert(tech_count).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech count for user {pf['user']}") + continue + + if "userTechEventList" in pf_data: + for pf_list in pf_data["userTechEventList"]: + pf_list["user"] = user + + sql = Insert(tech_event).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech event for user {pf['user']}") + continue + + for pf_list in pf_data["userTradeItemList"]: + pf_list["user"] = user + + sql = Insert(trade_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item trade item for user {pf['user']}") + continue + + if "userEventMusicList" in pf_data: + for pf_list in pf_data["userEventMusicList"]: + pf_list["user"] = user + + sql = Insert(event_music).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event music for user {pf['user']}") + continue + + if "userEventPointList" in pf_data: + for pf_list in pf_data["userEventPointList"]: + pf_list["user"] = user + + sql = Insert(event_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event point for user {pf['user']}") + continue + + for pf_list in pf_data["userLoginBonusList"]: + pf_list["user"] = user + + sql = Insert(login_bonus).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item login bonus for user {pf['user']}") + continue + + for pf_list in pf_data["userMissionPointList"]: + pf_list["user"] = user + + sql = Insert(mission_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item mission point for user {pf['user']}") + continue + + for pf_list in pf_data["userScenarioList"]: + pf_list["user"] = user + + sql = Insert(scenerio).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item scenerio for user {pf['user']}") + continue + + if "userSessionlogList" in pf_data: + for pf_list in pf_data["userSessionlogList"]: + pf_list["user"] = user + + sql = Insert(session_log).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki log session for user {pf['user']}") + continue + + self.logger.info(f"Importing {len(ongeki_items)} ongeki items") + + for it in ongeki_items: + user = it["user"] + it_type = it["type"] + it_id = it["item_id"] + it_data = it["data"] + it_data["user"] = user + + if it_type == item_types["character"] and "characterId" in it_data: + sql = Insert(character).values(**it_data) + + elif it_type == item_types["story"]: + sql = Insert(story).values(**it_data) + + elif it_type == item_types["card"]: + sql = Insert(card).values(**it_data) + + elif it_type == item_types["deck"]: + sql = Insert(deck).values(**it_data) + + elif it_type == item_types["login"]: # login bonus + sql = Insert(login_bonus).values(**it_data) + + elif it_type == item_types["chapter"]: + sql = Insert(chapter).values(**it_data) + + else: + sql = Insert(item).values(**it_data) + + conflict = sql.on_duplicate_key_update(**it_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item {it_id} kind {it_type} for user {user}") + + self.logger.info(f"Importing {len(ongeki_scores)} ongeki scores") + + for sc in ongeki_scores: + user = sc["user"] + sc_data = sc["data"] + sc_data["user"] = user + + sql = Insert(score_best).values(**sc_data) + conflict = sql.on_duplicate_key_update(**sc_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score for user {user}: {sc['song_id']}/{sc['chart_id']}") + + else: + self.logger.info(f"Ongeki not found, skipping...") + + # Wacca + if os.path.exists("titles/wacca/schema"): + from titles.wacca.schema.profile import profile, option, bingo, gate, favorite + from titles.wacca.schema.item import item, ticket, song_unlock, trophy + from titles.wacca.schema.score import best_score, stageup + from titles.wacca.reverse import WaccaReverse + from titles.wacca.const import WaccaConstants + + default_opts = WaccaReverse.OPTIONS_DEFAULTS + opts = WaccaConstants.OPTIONS + item_types = WaccaConstants.ITEM_TYPES + + wacca_profiles = [] + wacca_items = [] + wacca_scores = [] + wacca_achievements = [] + + if "SDFE" in profiles: + wacca_profiles = profiles["SDFE"] + if "SDFE" in items: + wacca_items = items["SDFE"] + if "SDFE" in scores: + wacca_scores = scores["SDFE"] + if "SDFE" in achievements: + wacca_achievements = achievements["SDFE"] + + self.logger.info(f"Importing {len(wacca_profiles)} wacca profiles") + + for pf in wacca_profiles: + if pf["version"] == 0 or pf["version"] == 1: + season = 1 + elif pf["version"] == 2 or pf["version"] == 3: + season = 2 + elif pf["version"] >= 4: + season = 3 + + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + + try: + sql = Insert(profile).values( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + conflict = sql.on_duplicate_key_update( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca profile for user {pf['user']}") + continue + + for opt, val in pf["data"]["option"].items(): + if val != default_opts[opt]: + opt_id = opts[opt] + sql = Insert(option).values( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca option for user {pf['user']} {opt} -> {val}") + + except KeyError as e: + self.logger.warn(f"Outdated wacca profile, skipping: {e}") + + if "gate" in pf["data"]: + for profile_gate in pf["data"]["gate"]: + sql = Insert(gate).values( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca gate for user {pf['user']} -> {profile_gate['id']}") + continue + + if "favorite" in pf["data"]: + for profile_favorite in pf["data"]["favorite"]: + sql = Insert(favorite).values( + user = pf["user"], + song_id = profile_favorite + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + song_id = profile_favorite + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca favorite songs for user {pf['user']} -> {profile_favorite}") + continue + + for it in wacca_items: + user = it["user"] + item_type = it["type"] + item_id = it["item_id"] + + if type(it["data"]) is not dict: + it["data"] = json.loads(it["data"]) + + if item_type == item_types["ticket"]: + if "quantity" in it["data"]: + for x in range(it["data"]["quantity"]): + sql = Insert(ticket).values( + user = user, + ticket_id = item_id, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + ticket_id = item_id, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert ticket {item_id} for user {user}") + + elif item_type == item_types["music_unlock"] or item_type == item_types["music_difficulty_unlock"]: + diff = 0 + if "difficulty" in it["data"]: + for x in it["data"]["difficulty"]: + if x == 1: + diff += 1 + else: + break + + sql = Insert(song_unlock).values( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert song unlock {item_id} {diff} for user {user}") + + elif item_type == item_types["trophy"]: + season = int(item_id / 100000) + sql = Insert(trophy).values( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + + conflict = sql.on_duplicate_key_update( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + else: + sql = Insert(item).values( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + + conflict = sql.on_duplicate_key_update( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + for sc in wacca_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + sql = Insert(best_score).values( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + + conflict = sql.on_duplicate_key_update( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca score for user {sc['user']} {int(sc['song_id'])} {sc['chart_id']}") + + for ach in wacca_achievements: + if ach["version"] == 0 or ach["version"] == 1: + season = 1 + elif ach["version"] == 2 or ach["version"] == 3: + season = 2 + elif ach["version"] >= 4: + season = 3 + + if type(ach["data"]) is not dict: + ach["data"] = json.loads(ach["data"]) + + sql = Insert(stageup).values( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + + conflict = sql.on_duplicate_key_update( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca achievement for user {ach['user']}") + + else: + self.logger.info(f"Wacca not found, skipping...") From db6b950c295ca78618b87654def4b204ba4e6c6f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 15:40:25 -0500 Subject: [PATCH 017/589] add partial frontend --- core/__init__.py | 1 + core/data/database.py | 2 +- core/data/schema/user.py | 88 ++++++++- core/data/schema/versions/CORE_1_rollback.sql | 2 + core/data/schema/versions/CORE_2_upgrade.sql | 2 + core/frontend.py | 176 ++++++++++++++++++ core/frontend/gate/create.jinja | 24 +++ core/frontend/gate/gate.jinja | 17 ++ core/frontend/index.jinja | 88 +++++++++ core/frontend/user/index.jinja | 4 + core/frontend/widgets/err_banner.jinja | 14 ++ core/frontend/widgets/topbar.jinja | 6 + index.py | 4 + requirements.txt | 2 + requirements_win.txt | 2 + titles/wacca/__init__.py | 2 + titles/wacca/frontend.py | 18 ++ 17 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 core/data/schema/versions/CORE_1_rollback.sql create mode 100644 core/data/schema/versions/CORE_2_upgrade.sql create mode 100644 core/frontend.py create mode 100644 core/frontend/gate/create.jinja create mode 100644 core/frontend/gate/gate.jinja create mode 100644 core/frontend/index.jinja create mode 100644 core/frontend/user/index.jinja create mode 100644 core/frontend/widgets/err_banner.jinja create mode 100644 core/frontend/widgets/topbar.jinja create mode 100644 titles/wacca/frontend.py diff --git a/core/__init__.py b/core/__init__.py index c72ba0a..717de33 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -4,3 +4,4 @@ from core.aimedb import AimedbFactory from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet +from core.frontend import FrontendServlet \ No newline at end of file diff --git a/core/data/database.py b/core/data/database.py index 963d016..70fc3e0 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 = 1 + self.schema_ver_latest = 2 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 7d76bbe..9e79891 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,9 +1,14 @@ from enum import Enum from typing import Dict, Optional -from sqlalchemy import Table, Column +from sqlalchemy import Table, Column, and_ 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.engine import Row from core.data.schema.base import BaseData, metadata @@ -26,6 +31,7 @@ frontend_session = Table( 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' @@ -37,21 +43,83 @@ class PermissionBits(Enum): PermSysAdmin = 4 class UserData(BaseData): - def create_user(self, username: str = None, email: str = None, password: str = None) -> Optional[int]: - + 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 = None - else: - permission = 0 + permission = 1 - sql = aime_user.insert().values(username=username, email=email, password=password, permissions=permission) + if id is None: + sql = insert(aime_user).values( + username=username, + email=email, + password=password, + permissions=permission + ) + else: + sql = insert(aime_user).values( + id=id, + username=username, + email=email, + password=password, + permissions=permission + ) + + conflict = sql.on_duplicate_key_update( + username=username, + email=email, + password=password, + permissions=permission + ) - result = self.execute(sql) + result = self.execute(conflict) 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) + + result = self.execute(sql) + if result is None: return False + return True + + 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 + + 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 + def reset_autoincrement(self, ai_value: int) -> None: - # Didn't feel like learning how to do this the right way - # if somebody wants a free PR go nuts I guess + # ALTER TABLE isn't in sqlalchemy so we do this the ugly way sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}" self.execute(sql) \ No newline at end of file diff --git a/core/data/schema/versions/CORE_1_rollback.sql b/core/data/schema/versions/CORE_1_rollback.sql new file mode 100644 index 0000000..8a1144b --- /dev/null +++ b/core/data/schema/versions/CORE_1_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +DROP COLUMN `ip`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_2_upgrade.sql b/core/data/schema/versions/CORE_2_upgrade.sql new file mode 100644 index 0000000..44deb6d --- /dev/null +++ b/core/data/schema/versions/CORE_2_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +ADD `ip` CHAR(15); \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py new file mode 100644 index 0000000..554df13 --- /dev/null +++ b/core/frontend.py @@ -0,0 +1,176 @@ +import logging, coloredlogs +from typing import Any, Dict +from twisted.web import resource +from twisted.web.util import redirectTo +from twisted.web.http import Request +from logging.handlers import TimedRotatingFileHandler +import jinja2 +import bcrypt + +from core.config import CoreConfig +from core.data import Data +from core.utils import Utils + +class FrontendServlet(resource.Resource): + children: Dict[str, Any] = {} + def getChild(self, name: bytes, request: Request): + self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") + if name == b'': + return self + return resource.Resource.getChild(self, name, request) + + def __init__(self, cfg: CoreConfig, config_dir: str) -> None: + self.config = cfg + log_fmt_str = "[%(asctime)s] Frontend | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + self.logger = logging.getLogger("frontend") + self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader("core/frontend")) + + 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) + + fe_game = FE_Game(cfg, self.environment) + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + if hasattr(game_mod, "frontend"): + try: + fe_game.putChild(game_dir.encode(), game_mod.frontend(cfg, self.environment, config_dir)) + except: + raise + + 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") + + def render_GET(self, request): + self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + template = self.environment.get_template("index.jinja") + return template.render(server_name=self.config.server.name, title=self.config.server.name).encode("utf-16") + +class FE_Base(resource.Resource): + """ + A Generic skeleton class that all frontend handlers should inherit from + Initializes the environment, data, logger, config, and sets isLeaf to true + It is expected that game implementations of this class overwrite many of these + """ + isLeaf = True + def __init__(self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str = None) -> None: + self.core_config = cfg + self.data = Data(cfg) + self.logger = logging.getLogger('frontend') + self.environment = environment + +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() + if uri.startswith("/gate/create"): + return self.create_user(request) + + if b'e' in request.args: + try: + err = int(request.args[b'e'][0].decode()) + except: + err = 0 + + else: err = 0 + + template = self.environment.get_template("gate/gate.jinja") + return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err).encode("utf-16") + + def render_POST(self, request: Request): + uri = request.uri.decode() + ip = request.getClientAddress().host + + if uri == "/gate/gate.login": + access_code: str = request.args[b"access_code"][0].decode() + passwd: str = request.args[b"passwd"][0] + if passwd == b"": + passwd = None + + 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.login(uid, ip=ip) + + 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: + return redirectTo(b"/gate?e=1", request) + + request.addCookie('session', sesh) + return redirectTo(b"/user", request) + + elif uri == "/gate/gate.create": + access_code: str = request.args[b"access_code"][0].decode() + username: str = request.args[b"username"][0] + email: str = request.args[b"email"][0].decode() + passwd: str = request.args[b"passwd"][0] + + uid = self.data.card.get_user_id_from_card(access_code) + if uid is None: + return redirectTo(b"/gate?e=1", request) + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(passwd, salt) + + result = self.data.user.create_user(uid, username, email, hashed.decode(), 1) + if result is None: + return redirectTo(b"/gate?e=3", request) + + sesh = self.data.user.login(uid, hashed, ip) + if sesh is None: + return redirectTo(b"/gate", request) + request.addCookie('session', sesh) + + return redirectTo(b"/user", request) + + else: + return b"" + + def create_user(self, request: Request): + if b'ac' not in request.args or len(request.args[b'ac'][0].decode()) != 20: + return redirectTo(b"/gate?e=2", request) + + ac = request.args[b'ac'][0].decode() + + template = self.environment.get_template("gate/create.jinja") + return template.render(title=f"{self.core_config.server.name} | Create User", code=ac).encode("utf-16") + +class FE_User(FE_Base): + def render_GET(self, request: Request): + template = self.environment.get_template("user/index.jinja") + return template.render().encode("utf-16") + if b'session' not in request.cookies: + return redirectTo(b"/gate", request) + +class FE_Game(FE_Base): + isLeaf = False + children: Dict[str, Any] = {} + + def getChild(self, name: bytes, request: Request): + if name == b'': + return self + return resource.Resource.getChild(self, name, request) + + def render_GET(self, request: Request) -> bytes: + return redirectTo(b"/user", request) \ No newline at end of file diff --git a/core/frontend/gate/create.jinja b/core/frontend/gate/create.jinja new file mode 100644 index 0000000..f5b78ae --- /dev/null +++ b/core/frontend/gate/create.jinja @@ -0,0 +1,24 @@ +{% extends "index.jinja" %} +{% block content %} +

Create User

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

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

Gate

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

+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/index.jinja b/core/frontend/index.jinja new file mode 100644 index 0000000..c70688b --- /dev/null +++ b/core/frontend/index.jinja @@ -0,0 +1,88 @@ + + + + {{ title }} + + + + + + {% include "widgets/topbar.jinja" %} + {% block content %} +

{{ server_name }}

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

testing

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

Error

+{% if error == 1 %} +Card not registered, or wrong password +{% elif error == 2 %} +Missing or malformed access code +{% elif error == 3 %} +Failed to create user +{% else %} +An unknown error occoured +{% endif %} +
+{% endif %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja new file mode 100644 index 0000000..5dbba5d --- /dev/null +++ b/core/frontend/widgets/topbar.jinja @@ -0,0 +1,6 @@ +
+ Navigation +
+
+ +
\ No newline at end of file diff --git a/index.py b/index.py index 0049427..5344ce1 100644 --- a/index.py +++ b/index.py @@ -95,6 +95,7 @@ if __name__ == "__main__": 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}" billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" if cfg.server.is_develop: @@ -106,6 +107,9 @@ if __name__ == "__main__": endpoints.serverFromString(reactor, allnet_server_str).listen(server.Site(dispatcher)) endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg)) + if cfg.frontend.enable: + endpoints.serverFromString(reactor, frontend_server_str).listen(server.Site(FrontendServlet(cfg, args.config))) + if cfg.billing.port > 0: endpoints.serverFromString(reactor, billing_server_str).listen(server.Site(dispatcher)) diff --git a/requirements.txt b/requirements.txt index 40dbf84..70b4215 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ coloredlogs pylibmc wacky Routes +bcrypt +jinja2 diff --git a/requirements_win.txt b/requirements_win.txt index 89527fe..d36e884 100644 --- a/requirements_win.txt +++ b/requirements_win.txt @@ -12,3 +12,5 @@ inflection coloredlogs wacky Routes +bcrypt +jinja2 diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index c0351da..41d8dc2 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -2,10 +2,12 @@ from titles.wacca.const import WaccaConstants from titles.wacca.index import WaccaServlet from titles.wacca.read import WaccaReader from titles.wacca.database import WaccaData +from titles.wacca.frontend import WaccaFrontend index = WaccaServlet database = WaccaData reader = WaccaReader +frontend = WaccaFrontend use_default_title = True include_protocol = True diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py new file mode 100644 index 0000000..a61d5f4 --- /dev/null +++ b/titles/wacca/frontend.py @@ -0,0 +1,18 @@ +import yaml +import jinja2 +from twisted.web.http import Request + +from core.frontend import FE_Base +from core.config import CoreConfig +from titles.wacca.database import WaccaData +from titles.wacca.config import WaccaConfig + +class WaccaFrontend(FE_Base): + def __init__(self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str) -> None: + super().__init__(cfg, environment) + self.data = WaccaData(cfg) + self.game_cfg = WaccaConfig() + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + + def render_GET(self, request: Request) -> bytes: + return b"" From a7821fade886282f3218359ed8a746313df818c4 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 19 Feb 2023 22:56:09 +0100 Subject: [PATCH 018/589] diva: improved update_profile() function --- titles/diva/base.py | 79 +++++++++++++++++++---------------- titles/diva/schema/profile.py | 33 ++++----------- 2 files changed, 51 insertions(+), 61 deletions(-) diff --git a/titles/diva/base.py b/titles/diva/base.py index 7ad5793..2e788af 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -162,7 +162,10 @@ class DivaBase(): new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"]) - self.data.profile.update_profile(data["pd_id"], profile["lv_num"], profile["lv_pnt"], new_vcld_pts, profile["hp_vol"], profile["btn_se_vol"], profile["btn_se_vol2"], profile["sldr_se_vol2"], profile["sort_kind"], profile["use_pv_mdl_eqp"], profile["use_mdl_pri"], profile["use_pv_skn_eqp"], profile["use_pv_btn_se_eqp"], profile["use_pv_sld_se_eqp"], profile["use_pv_chn_sld_se_eqp"], profile["use_pv_sldr_tch_se_eqp"], profile["nxt_pv_id"], profile["nxt_dffclty"], profile["nxt_edtn"], profile["dsp_clr_brdr"], profile["dsp_intrm_rnk"], profile["dsp_clr_sts"], profile["rgo_sts"], profile["lv_efct_id"], profile["lv_plt_id"], profile["my_qst_id"], profile["my_qst_sts"]) + 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 @@ -214,9 +217,10 @@ class DivaBase(): new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"]) # save new Vocaloid Points balance - profile = dict(profile) - profile["vcld_pts"] = new_vcld_pts - self.data.profile.update_profile(profile) + 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"]) @@ -613,21 +617,21 @@ class DivaBase(): response += f"&lv_pnt_old={int(profile['lv_pnt'])}" # update the profile and commit changes to the db - profile = dict(profile) - profile["lv_num"] = new_level - profile["lv_pnt"] = new_level_pnt - profile["vcld_pts"] = int(data["vcld_pts"]) - profile["hp_vol"] = int(data["hp_vol"]) - profile["btn_se_vol"] = int(data["btn_se_vol"]) - profile["sldr_se_vol2"] = int(data["sldr_se_vol2"]) - profile["sort_kind"] = int(data["sort_kind"]) - profile["nxt_pv_id"] = int(data["ply_pv_id"]) - profile["nxt_dffclty"] = int(data["nxt_dffclty"]) - profile["nxt_edtn"] = int(data["nxt_edtn"]) - profile["my_qst_id"] = data["my_qst_id"] - profile["my_qst_sts"] = data["my_qst_sts"] - - self.data.profile.update_profile(profile) + self.data.profile.update_profile( + profile["user"], + lv_num=new_level, + lv_pnt=new_level_pnt, + vcld_pts=int(data["vcld_pts"]), + hp_vol=int(data["hp_vol"]), + btn_se_vol=int(data["btn_se_vol"]), + sldr_se_vol2=int(data["sldr_se_vol2"]), + sort_kind=int(data["sort_kind"]), + nxt_pv_id=int(data["ply_pv_id"]), + 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"] + ) response += f"&lv_num={new_level}" response += f"&lv_str={profile['lv_str']}" @@ -664,11 +668,11 @@ class DivaBase(): def handle_end_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) - profile = dict(profile) - profile["my_qst_id"] = data["my_qst_id"] - profile["my_qst_sts"] = data["my_qst_sts"] - - self.data.profile.update_profile(profile) + self.data.profile.update_profile( + profile["user"], + my_qst_id=data["my_qst_id"], + my_qst_sts=data["my_qst_sts"] + ) return (f'') def handle_shop_exit_request(self, data: Dict) -> Dict: @@ -711,28 +715,33 @@ class DivaBase(): return "&cd_adm_result=0" # update the vocaloid points and player name - profile = dict(profile) - profile["vcld_pts"] -= int(data["chg_name_price"]) - profile["player_name"] = data["player_name"] - - self.data.profile.update_profile(profile) + 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 + ) response = "&cd_adm_result=1" response += "&accept_idx=100" response += f"&pd_id={profile['user']}" - response += f"&player_name={profile['player_name']}" + 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) - # set password to true and update the saved password - profile = dict(profile) - profile["passwd_stat"] = 1 - profile["passwd"] = data["new_passwd"] + # TODO: return correct error number instead of 0 + if (data["passwd"] != profile["passwd"]): + return "&cd_adm_result=0" - self.data.profile.update_profile(profile) + # set password to true and update the saved password + self.data.profile.update_profile( + profile["user"], + passwd_stat=1, + passwd=data["new_passwd"] + ) response = "&cd_adm_result=1" response += "&accept_idx=100" diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 272026c..993b03c 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -76,36 +76,17 @@ class DivaProfileData(BaseData): return None return result.lastrowid - def update_profile(self, profile_updated: Dict) -> None: - sql = profile.update(profile.c.user == profile_updated["user"]).values( - player_name=profile_updated["player_name"], - lv_num=profile_updated["lv_num"], - lv_pnt=profile_updated["lv_pnt"], - vcld_pts=profile_updated["vcld_pts"], - hp_vol=profile_updated["hp_vol"], - btn_se_vol=profile_updated["btn_se_vol"], - btn_se_vol2=profile_updated["btn_se_vol2"], - sldr_se_vol2=profile_updated["sldr_se_vol2"], - sort_kind=profile_updated["sort_kind"], - nxt_pv_id=profile_updated["nxt_pv_id"], - nxt_dffclty=profile_updated["nxt_dffclty"], - nxt_edtn=profile_updated["nxt_edtn"], - dsp_clr_brdr=profile_updated["dsp_clr_brdr"], - dsp_intrm_rnk=profile_updated["dsp_intrm_rnk"], - dsp_clr_sts=profile_updated["dsp_clr_sts"], - rgo_sts=profile_updated["rgo_sts"], - lv_efct_id=profile_updated["lv_efct_id"], - lv_plt_id=profile_updated["lv_plt_id"], - my_qst_id=profile_updated["my_qst_id"], - my_qst_sts=profile_updated["my_qst_sts"], - passwd_stat=profile_updated["passwd_stat"], - passwd=profile_updated["passwd"] - ) + def update_profile(self, aime_id: int, **profile_args) -> None: + """ + Given an aime_id update the profile corresponding to the arguments + which are the diva_profile Columns + """ + sql = profile.update(profile.c.user == aime_id).values(**profile_args) result = self.execute(sql) if result is None: self.logger.error( - f"update_profile: failed to update profile! profile: {profile_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]]: From b3432280720d596bbc2d1b2cc3cd304374527e52 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 20 Feb 2023 21:55:12 -0500 Subject: [PATCH 019/589] refactor template directory to be artemis root dir --- core/frontend.py | 27 +++++++++++++++---------- core/frontend/gate/create.jinja | 2 +- core/frontend/gate/gate.jinja | 4 ++-- core/frontend/index.jinja | 2 +- core/frontend/user/index.jinja | 2 +- core/frontend/widgets/topbar.jinja | 9 ++++++++- titles/wacca/frontend.py | 8 +++++++- titles/wacca/frontend/wacca_index.jinja | 4 ++++ 8 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 titles/wacca/frontend/wacca_index.jinja diff --git a/core/frontend.py b/core/frontend.py index 554df13..780698e 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -1,5 +1,5 @@ import logging, coloredlogs -from typing import Any, Dict +from typing import Any, Dict, List from twisted.web import resource from twisted.web.util import redirectTo from twisted.web.http import Request @@ -12,7 +12,6 @@ from core.data import Data from core.utils import Utils class FrontendServlet(resource.Resource): - children: Dict[str, Any] = {} def getChild(self, name: bytes, request: Request): self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") if name == b'': @@ -24,7 +23,9 @@ class FrontendServlet(resource.Resource): log_fmt_str = "[%(asctime)s] Frontend | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("frontend") - self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader("core/frontend")) + self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader(".")) + self.game_list: List[Dict[str, str]] = [] + self.children: Dict[str, Any] = {} fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "frontend"), when="d", backupCount=10) fileHandler.setFormatter(log_fmt) @@ -43,10 +44,13 @@ class FrontendServlet(resource.Resource): for game_dir, game_mod in games.items(): if hasattr(game_mod, "frontend"): try: - fe_game.putChild(game_dir.encode(), game_mod.frontend(cfg, self.environment, config_dir)) + game_fe = game_mod.frontend(cfg, self.environment, config_dir) + self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) + fe_game.putChild(game_dir.encode(), game_fe) except: 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) @@ -55,8 +59,8 @@ class FrontendServlet(resource.Resource): def render_GET(self, request): self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") - template = self.environment.get_template("index.jinja") - return template.render(server_name=self.config.server.name, title=self.config.server.name).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).encode("utf-16") class FE_Base(resource.Resource): """ @@ -65,11 +69,12 @@ class FE_Base(resource.Resource): It is expected that game implementations of this class overwrite many of these """ isLeaf = True - def __init__(self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str = None) -> None: + def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None: self.core_config = cfg self.data = Data(cfg) self.logger = logging.getLogger('frontend') self.environment = environment + self.nav_name = "nav_name" class FE_Gate(FE_Base): def render_GET(self, request: Request): @@ -86,7 +91,7 @@ class FE_Gate(FE_Base): else: err = 0 - template = self.environment.get_template("gate/gate.jinja") + 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") def render_POST(self, request: Request): @@ -153,12 +158,12 @@ class FE_Gate(FE_Base): ac = request.args[b'ac'][0].decode() - template = self.environment.get_template("gate/create.jinja") + 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") class FE_User(FE_Base): def render_GET(self, request: Request): - template = self.environment.get_template("user/index.jinja") + template = self.environment.get_template("core/frontend/user/index.jinja") return template.render().encode("utf-16") if b'session' not in request.cookies: return redirectTo(b"/gate", request) diff --git a/core/frontend/gate/create.jinja b/core/frontend/gate/create.jinja index f5b78ae..f8b5e51 100644 --- a/core/frontend/gate/create.jinja +++ b/core/frontend/gate/create.jinja @@ -1,4 +1,4 @@ -{% extends "index.jinja" %} +{% extends "core/frontend/index.jinja" %} {% block content %}

Create User

diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja index 85b3ea5..760fbab 100644 --- a/core/frontend/gate/gate.jinja +++ b/core/frontend/gate/gate.jinja @@ -1,7 +1,7 @@ -{% extends "index.jinja" %} +{% extends "core/frontend/index.jinja" %} {% block content %}

Gate

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

diff --git a/core/frontend/index.jinja b/core/frontend/index.jinja index c70688b..7e4a1ca 100644 --- a/core/frontend/index.jinja +++ b/core/frontend/index.jinja @@ -80,7 +80,7 @@ - {% include "widgets/topbar.jinja" %} + {% include "core/frontend/widgets/topbar.jinja" %} {% block content %}

{{ server_name }}

{% endblock content %} diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja index dc62294..eabdd18 100644 --- a/core/frontend/user/index.jinja +++ b/core/frontend/user/index.jinja @@ -1,4 +1,4 @@ -{% extends "index.jinja" %} +{% extends "core/frontend/index.jinja" %} {% block content %}

testing

{% endblock content %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja index 5dbba5d..6bef3e3 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/frontend/widgets/topbar.jinja @@ -1,6 +1,13 @@
Navigation
-
+
+   + {% for game in game_list %} +   + {% endfor %} +
+
+ \ No newline at end of file diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index a61d5f4..b7232b8 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -6,6 +6,7 @@ from core.frontend import FE_Base from core.config import CoreConfig from titles.wacca.database import WaccaData from titles.wacca.config import WaccaConfig +from titles.wacca.const import WaccaConstants class WaccaFrontend(FE_Base): def __init__(self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str) -> None: @@ -13,6 +14,11 @@ class WaccaFrontend(FE_Base): 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: - return b"" + 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"] + ).encode("utf-16") diff --git a/titles/wacca/frontend/wacca_index.jinja b/titles/wacca/frontend/wacca_index.jinja new file mode 100644 index 0000000..6a5f046 --- /dev/null +++ b/titles/wacca/frontend/wacca_index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Wacca

+{% endblock content %} \ No newline at end of file From a83b717821f1937cb325de1ef11f2cbe3f7c76f1 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 19 Feb 2023 14:52:20 -0500 Subject: [PATCH 020/589] carry over database functions from megaime --- core/data/database.py | 1703 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1702 insertions(+), 1 deletion(-) diff --git a/core/data/database.py b/core/data/database.py index 800b5d0..963d016 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -4,11 +4,14 @@ 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 from hashlib import sha256 from core.config import CoreConfig from core.data.schema import * +from core.utils import Utils class Data: def __init__(self, cfg: CoreConfig) -> None: @@ -50,4 +53,1702 @@ class Data: coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str) self.logger.handler_set = True # type: ignore - \ No newline at end of file + def create_database(self): + self.logger.info("Creating databases...") + try: + metadata.create_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to create databases! {e}") + return + + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + try: + 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]) + + except Exception as e: + self.logger.warning(f"Could not load database schema from {game_dir} - {e}") + + self.logger.info(f"Setting base_schema_ver to {self.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) + + def recreate_database(self): + self.logger.info("Dropping all databases...") + self.base.execute("SET FOREIGN_KEY_CHECKS=0") + try: + metadata.drop_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to drop databases! {e}") + return + + for root, dirs, files in os.walk("./titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") + + try: + 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}") + + except ImportError as e: + self.logger.warning(f"Failed to load database schema dir {dir} - {e}") + break + + self.base.execute("SET FOREIGN_KEY_CHECKS=1") + + self.create_database() + + def migrate_database(self, game: str, version: 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") + 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 dump_db(self): + dbname = self.config.database.name + + self.logger.info("Database dumper for use with the reworked schema") + self.logger.info("Dumping users...") + + sql = f"SELECT * FROM `{dbname}`.`user`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + users = result.fetchall() + + user_list: List[Dict[str, Any]] = [] + for usr in users: + user_list.append({ + "id": usr["id"], + "username": usr["username"], + "email": usr["email"], + "password": usr["password"], + "permissions": usr["permissions"], + "created_date": datetime.strftime(usr["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(usr["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(user_list)} users") + with open("dbdump-user.json", "w", encoding="utf-8") as f: + f.write(json.dumps(user_list)) + self.logger.info(f"Saved as dbdump-user.json") + + self.logger.info("Dumping cards...") + + sql = f"SELECT * FROM `{dbname}`.`card`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + cards = result.fetchall() + + card_list: List[Dict[str, Any]] = [] + for crd in cards: + card_list.append({ + "id": crd["id"], + "user": crd["user"], + "access_code": crd["access_code"], + "is_locked": crd["is_locked"], + "is_banned": crd["is_banned"], + "created_date": datetime.strftime(crd["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(crd["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(card_list)} cards") + with open("dbdump-card.json", "w", encoding="utf-8") as f: + f.write(json.dumps(card_list)) + self.logger.info(f"Saved as dbdump-card.json") + + self.logger.info("Dumping arcades...") + + sql = f"SELECT * FROM `{dbname}`.`arcade`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcades = result.fetchall() + + arcade_list: List[Dict[str, Any]] = [] + for arc in arcades: + arcade_list.append({ + "id": arc["id"], + "name": arc["name"], + "nickname": arc["name"], + "country": None, + "country_id": None, + "state": None, + "city": None, + "region_id": None, + "timezone": None, + }) + + self.logger.info(f"Done, found {len(arcade_list)} arcades") + with open("dbdump-arcade.json", "w", encoding="utf-8") as f: + f.write(json.dumps(arcade_list)) + self.logger.info(f"Saved as dbdump-arcade.json") + + self.logger.info("Dumping machines...") + + sql = f"SELECT * FROM `{dbname}`.`machine`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + machines = result.fetchall() + + machine_list: List[Dict[str, Any]] = [] + for mech in machines: + if "country" in mech["data"]: + country = mech["data"]["country"] + else: + country = None + + if "ota_enable" in mech["data"]: + ota_enable = mech["data"]["ota_enable"] + else: + ota_enable = None + + machine_list.append({ + "id": mech["id"], + "arcade": mech["arcade"], + "serial": mech["keychip"], + "game": mech["game"], + "board": None, + "country": country, + "timezone": None, + "ota_enable": ota_enable, + "is_cab": False, + }) + + self.logger.info(f"Done, found {len(machine_list)} machines") + with open("dbdump-machine.json", "w", encoding="utf-8") as f: + f.write(json.dumps(machine_list)) + self.logger.info(f"Saved as dbdump-machine.json") + + self.logger.info("Dumping arcade owners...") + + sql = f"SELECT * FROM `{dbname}`.`arcade_owner`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcade_owners = result.fetchall() + + owner_list: List[Dict[str, Any]] = [] + for owner in owner_list: + owner_list.append(owner._asdict()) + + self.logger.info(f"Done, found {len(owner_list)} arcade owners") + with open("dbdump-arcade_owner.json", "w", encoding="utf-8") as f: + f.write(json.dumps(owner_list)) + self.logger.info(f"Saved as dbdump-arcade_owner.json") + + self.logger.info("Dumping profiles...") + + sql = f"SELECT * FROM `{dbname}`.`profile`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + profiles = result.fetchall() + + profile_list: Dict[List[Dict[str, Any]]] = {} + for pf in profiles: + game = pf["game"] + + if game not in profile_list: + profile_list[game] = [] + + profile_list[game].append({ + "id": pf["id"], + "user": pf["user"], + "version": pf["version"], + "use_count": pf["use_count"], + "name": pf["name"], + "game_id": pf["game_id"], + "mods": pf["mods"], + "data": pf["data"], + }) + + self.logger.info(f"Done, found profiles for {len(profile_list)} games") + with open("dbdump-profile.json", "w", encoding="utf-8") as f: + f.write(json.dumps(profile_list)) + self.logger.info(f"Saved as dbdump-profile.json") + + self.logger.info("Dumping scores...") + + sql = f"SELECT * FROM `{dbname}`.`score`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + scores = result.fetchall() + + score_list: Dict[List[Dict[str, Any]]] = {} + for sc in scores: + game = sc["game"] + + if game not in score_list: + score_list[game] = [] + + score_list[game].append({ + "id": sc["id"], + "user": sc["user"], + "version": sc["version"], + "song_id": sc["song_id"], + "chart_id": sc["chart_id"], + "score1": sc["score1"], + "score2": sc["score2"], + "fc1": sc["fc1"], + "fc2": sc["fc2"], + "cleared": sc["cleared"], + "grade": sc["grade"], + "data": sc["data"], + }) + + self.logger.info(f"Done, found scores for {len(score_list)} games") + with open("dbdump-score.json", "w", encoding="utf-8") as f: + f.write(json.dumps(score_list)) + self.logger.info(f"Saved as dbdump-score.json") + + self.logger.info("Dumping achievements...") + + sql = f"SELECT * FROM `{dbname}`.`achievement`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + achievements = result.fetchall() + + achievement_list: Dict[List[Dict[str, Any]]] = {} + for ach in achievements: + game = ach["game"] + + if game not in achievement_list: + achievement_list[game] = [] + + achievement_list[game].append({ + "id": ach["id"], + "user": ach["user"], + "version": ach["version"], + "type": ach["type"], + "achievement_id": ach["achievement_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found achievements for {len(achievement_list)} games") + with open("dbdump-achievement.json", "w", encoding="utf-8") as f: + f.write(json.dumps(achievement_list)) + self.logger.info(f"Saved as dbdump-achievement.json") + + self.logger.info("Dumping items...") + + sql = f"SELECT * FROM `{dbname}`.`item`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + items = result.fetchall() + + item_list: Dict[List[Dict[str, Any]]] = {} + for itm in items: + game = itm["game"] + + if game not in item_list: + item_list[game] = [] + + item_list[game].append({ + "id": itm["id"], + "user": itm["user"], + "version": itm["version"], + "type": itm["type"], + "item_id": itm["item_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found items for {len(item_list)} games") + with open("dbdump-item.json", "w", encoding="utf-8") as f: + f.write(json.dumps(item_list)) + self.logger.info(f"Saved as dbdump-item.json") + + def restore_from_old_schema(self): + # Import the tables we expect to be there + from core.data.schema.user import aime_user + from core.data.schema.card import aime_card + from core.data.schema.arcade import arcade, machine, arcade_owner + from sqlalchemy.dialects.mysql import Insert + + # Make sure that all the tables we're trying to access exist + self.create_database() + + # Import the data, making sure that dependencies are accounted for + if os.path.exists("dbdump-user.json"): + users = [] + with open("dbdump-user.json", "r", encoding="utf-8") as f: + users = json.load(f) + + self.logger.info(f"Load {len(users)} users") + + for user in users: + sql = Insert(aime_user).values(**user) + + conflict = sql.on_duplicate_key_update(**user) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert user {user['id']}") + continue + self.logger.info(f"Inserted user {user['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-card.json"): + cards = [] + with open("dbdump-card.json", "r", encoding="utf-8") as f: + cards = json.load(f) + + self.logger.info(f"Load {len(cards)} cards") + + for card in cards: + sql = Insert(aime_card).values(**card) + + conflict = sql.on_duplicate_key_update(**card) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert card {card['id']}") + continue + self.logger.info(f"Inserted card {card['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade.json"): + arcades = [] + with open("dbdump-arcade.json", "r", encoding="utf-8") as f: + arcades = json.load(f) + + self.logger.info(f"Load {len(arcades)} arcades") + + for ac in arcades: + sql = Insert(arcade).values(**ac) + + conflict = sql.on_duplicate_key_update(**ac) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade {ac['id']}") + continue + self.logger.info(f"Inserted arcade {ac['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade_owner.json"): + ac_owners = [] + with open("dbdump-arcade_owner.json", "r", encoding="utf-8") as f: + ac_owners = json.load(f) + + self.logger.info(f"Load {len(ac_owners)} arcade owners") + + for owner in ac_owners: + sql = Insert(arcade_owner).values(**owner) + + conflict = sql.on_duplicate_key_update(**owner) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade_owner {owner['user']}") + continue + self.logger.info(f"Inserted arcade_owner {owner['user']} -> {result.lastrowid}") + + if os.path.exists("dbdump-machine.json"): + mechs = [] + with open("dbdump-machine.json", "r", encoding="utf-8") as f: + mechs = json.load(f) + + self.logger.info(f"Load {len(mechs)} machines") + + for mech in mechs: + sql = Insert(machine).values(**mech) + + conflict = sql.on_duplicate_key_update(**mech) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert machine {mech['id']}") + continue + self.logger.info(f"Inserted machine {mech['id']} -> {result.lastrowid}") + + # Now the fun part, grabbing all our scores, profiles, items, and achievements and trying + # to conform them to our current, freeform schema. This will be painful... + profiles = {} + items = {} + scores = {} + achievements = {} + + if os.path.exists("dbdump-profile.json"): + with open("dbdump-profile.json", "r", encoding="utf-8") as f: + profiles = json.load(f) + + self.logger.info(f"Load {len(profiles)} profiles") + + if os.path.exists("dbdump-item.json"): + with open("dbdump-item.json", "r", encoding="utf-8") as f: + items = json.load(f) + + self.logger.info(f"Load {len(items)} items") + + if os.path.exists("dbdump-score.json"): + with open("dbdump-score.json", "r", encoding="utf-8") as f: + scores = json.load(f) + + self.logger.info(f"Load {len(scores)} scores") + + if os.path.exists("dbdump-achievement.json"): + with open("dbdump-achievement.json", "r", encoding="utf-8") as f: + achievements = json.load(f) + + self.logger.info(f"Load {len(achievements)} achievements") + + # Chuni / Chusan + if os.path.exists("titles/chuni/schema"): + from titles.chuni.schema.item import character, item, duel, map, map_area + from titles.chuni.schema.profile import profile, profile_ex, option, option_ex + from titles.chuni.schema.profile import recent_rating, activity, charge, emoney + from titles.chuni.schema.profile import overpower + from titles.chuni.schema.score import best_score, course + + chuni_profiles = [] + chuni_items = [] + chuni_scores = [] + + if "SDBT" in profiles: + chuni_profiles = profiles["SDBT"] + if "SDBT" in items: + chuni_items = items["SDBT"] + if "SDBT" in scores: + chuni_scores = scores["SDBT"] + if "SDHD" in profiles: + chuni_profiles += profiles["SDHD"] + if "SDHD" in items: + chuni_items += items["SDHD"] + if "SDHD" in scores: + chuni_scores += scores["SDHD"] + + self.logger.info(f"Importing {len(chuni_profiles)} chunithm/chunithm new profiles") + + for pf in chuni_profiles: + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + pf_data = pf["data"] + + # data + if "userData" in pf_data: + pf_data["userData"]["userName"] = bytes([ord(c) for c in pf_data["userData"]["userName"]]).decode("utf-8") + pf_data["userData"]["user"] = pf["user"] + pf_data["userData"]["version"] = pf["version"] + pf_data["userData"].pop("accessCode") + + if pf_data["userData"]["lastRomVersion"].startswith("2."): + pf_data["userData"]["version"] += 10 + + pf_data["userData"] = self.base.fix_bools(pf_data["userData"]) + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile for {pf['user']} ->{result.lastrowid}") + + # data_ex + if "userDataEx" in pf_data and len(pf_data["userDataEx"]) > 0: + pf_data["userDataEx"][0]["user"] = pf["user"] + pf_data["userDataEx"][0]["version"] = pf["version"] + + pf_data["userDataEx"] = self.base.fix_bools(pf_data["userDataEx"][0]) + + sql = Insert(profile_ex).values(**pf_data["userDataEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userDataEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile data_ex for {pf['user']} ->{result.lastrowid}") + + # option + if "userGameOption" in pf_data: + pf_data["userGameOption"]["user"] = pf["user"] + + pf_data["userGameOption"] = self.base.fix_bools(pf_data["userGameOption"]) + + sql = Insert(option).values(**pf_data["userGameOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOption"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile options for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile options for {pf['user']} ->{result.lastrowid}") + + # option_ex + if "userGameOptionEx" in pf_data and len(pf_data["userGameOptionEx"]) > 0: + pf_data["userGameOptionEx"][0]["user"] = pf["user"] + + pf_data["userGameOptionEx"] = self.base.fix_bools(pf_data["userGameOptionEx"][0]) + + sql = Insert(option_ex).values(**pf_data["userGameOptionEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOptionEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile option_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile option_ex for {pf['user']} ->{result.lastrowid}") + + # recent_rating + if "userRecentRatingList" in pf_data: + rr = { + "user": pf["user"], + "recentRating": pf_data["userRecentRatingList"] + } + + sql = Insert(recent_rating).values(**rr) + conflict = sql.on_duplicate_key_update(**rr) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile recent_rating for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile recent_rating for {pf['user']} ->{result.lastrowid}") + + # activity + if "userActivityList" in pf_data: + for act in pf_data["userActivityList"]: + act["user"] = pf["user"] + + sql = Insert(activity).values(**act) + conflict = sql.on_duplicate_key_update(**act) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile activity for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile activity for {pf['user']} ->{result.lastrowid}") + + # charge + if "userChargeList" in pf_data: + for cg in pf_data["userChargeList"]: + cg["user"] = pf["user"] + + cg = self.base.fix_bools(cg) + + sql = Insert(charge).values(**cg) + conflict = sql.on_duplicate_key_update(**cg) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile charge for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile charge for {pf['user']} ->{result.lastrowid}") + + # emoney + if "userEmoneyList" in pf_data: + for emon in pf_data["userEmoneyList"]: + emon["user"] = pf["user"] + + sql = Insert(emoney).values(**emon) + conflict = sql.on_duplicate_key_update(**emon) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile emoney for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile emoney for {pf['user']} ->{result.lastrowid}") + + # overpower + if "userOverPowerList" in pf_data: + for op in pf_data["userOverPowerList"]: + op["user"] = pf["user"] + + sql = Insert(overpower).values(**op) + conflict = sql.on_duplicate_key_update(**op) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile overpower for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile overpower for {pf['user']} ->{result.lastrowid}") + + # map_area + if "userMapAreaList" in pf_data: + for ma in pf_data["userMapAreaList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map_area).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map_area for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map_area for {pf['user']} ->{result.lastrowid}") + + #duel + if "userDuelList" in pf_data: + for ma in pf_data["userDuelList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(duel).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni duel for {pf['user']}") + else: + self.logger.info(f"Inserted chuni duel for {pf['user']} ->{result.lastrowid}") + + # map + if "userMapList" in pf_data: + for ma in pf_data["userMapList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map for {pf['user']} ->{result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_items)} chunithm/chunithm new items") + + for i in chuni_items: + if type(i["data"]) is not dict: + i["data"] = json.loads(i["data"]) + i_data = i["data"] + + i_data["user"] = i["user"] + + i_data = self.base.fix_bools(i_data) + + try: i_data.pop("assignIllust") + except: pass + + try: i_data.pop("exMaxLv") + except: pass + + if i["type"] == 20: #character + sql = Insert(character).values(**i_data) + else: + sql = Insert(item).values(**i_data) + + conflict = sql.on_duplicate_key_update(**i_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni item for user {i['user']}") + + else: + self.logger.info(f"Inserted chuni item for user {i['user']} {i['item_id']} -> {result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_scores)} chunithm/chunithm new scores") + + for sc in chuni_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + score_data = self.base.fix_bools(sc["data"]) + + try: score_data.pop("theoryCount") + except: pass + + try: score_data.pop("ext1") + except: pass + + score_data["user"] = sc["user"] + + sql = Insert(best_score).values(**score_data) + conflict = sql.on_duplicate_key_update(**score_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to put chuni score for user {sc['user']}") + else: + self.logger.info(f"Inserted chuni score for user {sc['user']} {sc['song_id']}/{sc['chart_id']} -> {result.lastrowid}") + + else: + self.logger.info(f"Chuni/Chusan not found, skipping...") + + # CXB + if os.path.exists("titles/cxb/schema"): + from titles.cxb.schema.item import energy + from titles.cxb.schema.profile import profile + from titles.cxb.schema.score import score, ranking + + cxb_profiles = [] + cxb_items = [] + cxb_scores = [] + + if "SDCA" in profiles: + cxb_profiles = profiles["SDCA"] + if "SDCA" in items: + cxb_items = items["SDCA"] + if "SDCA" in scores: + cxb_scores = scores["SDCA"] + + self.logger.info(f"Importing {len(cxb_profiles)} CXB profiles") + + for pf in cxb_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"]["data"] + pf_idx = pf["data"]["index"] + + for x in range(len(pf_data)): + sql = Insert(profile).values( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB profile for user {user} Index {pf_idx[x]}") + + self.logger.info(f"Importing {len(cxb_scores)} CXB scores") + + for sc in cxb_scores: + user = sc["user"] + version = sc["version"] + mcode = sc["data"]["mcode"] + index = sc["data"]["index"] + + sql = Insert(score).values( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB score for user {user} mcode {mcode}") + + self.logger.info(f"Importing {len(cxb_items)} CXB items") + + for it in cxb_items: + user = it["user"] + + if it["type"] == 3: # energy + sql = Insert(energy).values( + user = user, + energy = it["data"]["total"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + energy = it["data"]["total"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB energy for user {user}") + + elif it["type"] == 2: + sql = Insert(ranking).values( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + conflict = sql.on_duplicate_key_update( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB ranking for user {user}") + + else: + self.logger.error(f"Unknown CXB item type {it['type']} for user {user}") + + else: + self.logger.info(f"CXB not found, skipping...") + + # Diva + if os.path.exists("titles/diva/schema"): + from titles.diva.schema.profile import profile + from titles.diva.schema.score import score + from titles.diva.schema.item import shop + + diva_profiles = [] + diva_scores = [] + + if "SBZV" in profiles: + diva_profiles = profiles["SBZV"] + if "SBZV" in scores: + diva_scores = scores["SBZV"] + + self.logger.info(f"Importing {len(diva_profiles)} Diva profiles") + + for pf in diva_profiles: + pf["data"]["user"] = pf["user"] + pf["data"]["version"] = pf["version"] + pf_data = pf["data"] + + if "mdl_eqp_ary" in pf["data"]: + sql = Insert(shop).values( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"], + ) + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"] + ) + self.base.execute(conflict) + pf["data"].pop("mdl_eqp_ary") + + sql = Insert(profile).values(**pf_data) + conflict = sql.on_duplicate_key_update(**pf_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva profile for {pf['user']}") + + self.logger.info(f"Importing {len(diva_scores)} Diva scores") + + for sc in diva_scores: + user = sc["user"] + + clr_kind = -1 + for x in sc["data"]["stg_clr_kind"].split(","): + if x != "-1": + clr_kind = x + + cool_ct = 0 + for x in sc["data"]["stg_cool_cnt"].split(","): + if x != "0": + cool_ct = x + + fine_ct = 0 + for x in sc["data"]["stg_fine_cnt"].split(","): + if x != "0": + fine_ct = x + + safe_ct = 0 + for x in sc["data"]["stg_safe_cnt"].split(","): + if x != "0": + safe_ct = x + + sad_ct = 0 + for x in sc["data"]["stg_sad_cnt"].split(","): + if x != "0": + sad_ct = x + + worst_ct = 0 + for x in sc["data"]["stg_wt_wg_cnt"].split(","): + if x != "0": + worst_ct = x + + max_cmb = 0 + for x in sc["data"]["stg_max_cmb"].split(","): + if x != "0": + max_cmb = x + + sql = Insert(score).values( + user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb, + ) + + conflict = sql.on_duplicate_key_update(user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva score for {pf['user']}") + + else: + self.logger.info(f"Diva not found, skipping...") + + # Ongeki + if os.path.exists("titles/ongeki/schema"): + from titles.ongeki.schema.item import card, deck, character, boss, story + from titles.ongeki.schema.item import chapter, item, music_item, login_bonus + from titles.ongeki.schema.item import event_point, mission_point, scenerio + from titles.ongeki.schema.item import trade_item, event_music, tech_event + from titles.ongeki.schema.profile import profile, option, activity, recent_rating + from titles.ongeki.schema.profile import rating_log, training_room, kop + from titles.ongeki.schema.score import score_best, tech_count, playlog + from titles.ongeki.schema.log import session_log + + item_types = { + "character": 20, + "story": 21, + "card": 22, + "deck": 23, + "login": 24, + "chapter": 25 + } + + ongeki_profiles = [] + ongeki_items = [] + ongeki_scores = [] + + if "SDDT" in profiles: + ongeki_profiles = profiles["SDDT"] + if "SDDT" in items: + ongeki_items = items["SDDT"] + if "SDDT" in scores: + ongeki_scores = scores["SDDT"] + + self.logger.info(f"Importing {len(ongeki_profiles)} ongeki profiles") + + for pf in ongeki_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"] + + pf_data["userData"]["user"] = user + pf_data["userData"]["version"] = version + pf_data["userData"].pop("accessCode") + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile data for user {pf['user']}") + continue + + pf_data["userOption"]["user"] = user + + sql = Insert(option).values(**pf_data["userOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userOption"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile options for user {pf['user']}") + continue + + for pf_list in pf_data["userActivityList"]: + pf_list["user"] = user + + sql = Insert(activity).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile activity for user {pf['user']}") + continue + + sql = Insert(recent_rating).values( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + result = self.base.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert ongeki profile recent rating for user {pf['user']}") + continue + + for pf_list in pf_data["userRatinglogList"]: + pf_list["user"] = user + + sql = Insert(rating_log).values(**pf_list) + + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile rating log for user {pf['user']}") + continue + + for pf_list in pf_data["userTrainingRoomList"]: + pf_list["user"] = user + + sql = Insert(training_room).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + if "userKopList" in pf_data: + for pf_list in pf_data["userKopList"]: + pf_list["user"] = user + + sql = Insert(kop).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + for pf_list in pf_data["userBossList"]: + pf_list["user"] = user + + sql = Insert(boss).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item boss for user {pf['user']}") + continue + + for pf_list in pf_data["userDeckList"]: + pf_list["user"] = user + + sql = Insert(deck).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item deck for user {pf['user']}") + continue + + for pf_list in pf_data["userStoryList"]: + pf_list["user"] = user + + sql = Insert(story).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item story for user {pf['user']}") + continue + + for pf_list in pf_data["userChapterList"]: + pf_list["user"] = user + + sql = Insert(chapter).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item chapter for user {pf['user']}") + continue + + for pf_list in pf_data["userPlaylogList"]: + pf_list["user"] = user + + sql = Insert(playlog).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score playlog for user {pf['user']}") + continue + + for pf_list in pf_data["userMusicItemList"]: + pf_list["user"] = user + + sql = Insert(music_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item music item for user {pf['user']}") + continue + + for pf_list in pf_data["userTechCountList"]: + pf_list["user"] = user + + sql = Insert(tech_count).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech count for user {pf['user']}") + continue + + if "userTechEventList" in pf_data: + for pf_list in pf_data["userTechEventList"]: + pf_list["user"] = user + + sql = Insert(tech_event).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech event for user {pf['user']}") + continue + + for pf_list in pf_data["userTradeItemList"]: + pf_list["user"] = user + + sql = Insert(trade_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item trade item for user {pf['user']}") + continue + + if "userEventMusicList" in pf_data: + for pf_list in pf_data["userEventMusicList"]: + pf_list["user"] = user + + sql = Insert(event_music).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event music for user {pf['user']}") + continue + + if "userEventPointList" in pf_data: + for pf_list in pf_data["userEventPointList"]: + pf_list["user"] = user + + sql = Insert(event_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event point for user {pf['user']}") + continue + + for pf_list in pf_data["userLoginBonusList"]: + pf_list["user"] = user + + sql = Insert(login_bonus).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item login bonus for user {pf['user']}") + continue + + for pf_list in pf_data["userMissionPointList"]: + pf_list["user"] = user + + sql = Insert(mission_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item mission point for user {pf['user']}") + continue + + for pf_list in pf_data["userScenarioList"]: + pf_list["user"] = user + + sql = Insert(scenerio).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item scenerio for user {pf['user']}") + continue + + if "userSessionlogList" in pf_data: + for pf_list in pf_data["userSessionlogList"]: + pf_list["user"] = user + + sql = Insert(session_log).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki log session for user {pf['user']}") + continue + + self.logger.info(f"Importing {len(ongeki_items)} ongeki items") + + for it in ongeki_items: + user = it["user"] + it_type = it["type"] + it_id = it["item_id"] + it_data = it["data"] + it_data["user"] = user + + if it_type == item_types["character"] and "characterId" in it_data: + sql = Insert(character).values(**it_data) + + elif it_type == item_types["story"]: + sql = Insert(story).values(**it_data) + + elif it_type == item_types["card"]: + sql = Insert(card).values(**it_data) + + elif it_type == item_types["deck"]: + sql = Insert(deck).values(**it_data) + + elif it_type == item_types["login"]: # login bonus + sql = Insert(login_bonus).values(**it_data) + + elif it_type == item_types["chapter"]: + sql = Insert(chapter).values(**it_data) + + else: + sql = Insert(item).values(**it_data) + + conflict = sql.on_duplicate_key_update(**it_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item {it_id} kind {it_type} for user {user}") + + self.logger.info(f"Importing {len(ongeki_scores)} ongeki scores") + + for sc in ongeki_scores: + user = sc["user"] + sc_data = sc["data"] + sc_data["user"] = user + + sql = Insert(score_best).values(**sc_data) + conflict = sql.on_duplicate_key_update(**sc_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score for user {user}: {sc['song_id']}/{sc['chart_id']}") + + else: + self.logger.info(f"Ongeki not found, skipping...") + + # Wacca + if os.path.exists("titles/wacca/schema"): + from titles.wacca.schema.profile import profile, option, bingo, gate, favorite + from titles.wacca.schema.item import item, ticket, song_unlock, trophy + from titles.wacca.schema.score import best_score, stageup + from titles.wacca.reverse import WaccaReverse + from titles.wacca.const import WaccaConstants + + default_opts = WaccaReverse.OPTIONS_DEFAULTS + opts = WaccaConstants.OPTIONS + item_types = WaccaConstants.ITEM_TYPES + + wacca_profiles = [] + wacca_items = [] + wacca_scores = [] + wacca_achievements = [] + + if "SDFE" in profiles: + wacca_profiles = profiles["SDFE"] + if "SDFE" in items: + wacca_items = items["SDFE"] + if "SDFE" in scores: + wacca_scores = scores["SDFE"] + if "SDFE" in achievements: + wacca_achievements = achievements["SDFE"] + + self.logger.info(f"Importing {len(wacca_profiles)} wacca profiles") + + for pf in wacca_profiles: + if pf["version"] == 0 or pf["version"] == 1: + season = 1 + elif pf["version"] == 2 or pf["version"] == 3: + season = 2 + elif pf["version"] >= 4: + season = 3 + + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + + try: + sql = Insert(profile).values( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + conflict = sql.on_duplicate_key_update( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca profile for user {pf['user']}") + continue + + for opt, val in pf["data"]["option"].items(): + if val != default_opts[opt]: + opt_id = opts[opt] + sql = Insert(option).values( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca option for user {pf['user']} {opt} -> {val}") + + except KeyError as e: + self.logger.warn(f"Outdated wacca profile, skipping: {e}") + + if "gate" in pf["data"]: + for profile_gate in pf["data"]["gate"]: + sql = Insert(gate).values( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca gate for user {pf['user']} -> {profile_gate['id']}") + continue + + if "favorite" in pf["data"]: + for profile_favorite in pf["data"]["favorite"]: + sql = Insert(favorite).values( + user = pf["user"], + song_id = profile_favorite + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + song_id = profile_favorite + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca favorite songs for user {pf['user']} -> {profile_favorite}") + continue + + for it in wacca_items: + user = it["user"] + item_type = it["type"] + item_id = it["item_id"] + + if type(it["data"]) is not dict: + it["data"] = json.loads(it["data"]) + + if item_type == item_types["ticket"]: + if "quantity" in it["data"]: + for x in range(it["data"]["quantity"]): + sql = Insert(ticket).values( + user = user, + ticket_id = item_id, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + ticket_id = item_id, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert ticket {item_id} for user {user}") + + elif item_type == item_types["music_unlock"] or item_type == item_types["music_difficulty_unlock"]: + diff = 0 + if "difficulty" in it["data"]: + for x in it["data"]["difficulty"]: + if x == 1: + diff += 1 + else: + break + + sql = Insert(song_unlock).values( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert song unlock {item_id} {diff} for user {user}") + + elif item_type == item_types["trophy"]: + season = int(item_id / 100000) + sql = Insert(trophy).values( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + + conflict = sql.on_duplicate_key_update( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + else: + sql = Insert(item).values( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + + conflict = sql.on_duplicate_key_update( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + for sc in wacca_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + sql = Insert(best_score).values( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + + conflict = sql.on_duplicate_key_update( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca score for user {sc['user']} {int(sc['song_id'])} {sc['chart_id']}") + + for ach in wacca_achievements: + if ach["version"] == 0 or ach["version"] == 1: + season = 1 + elif ach["version"] == 2 or ach["version"] == 3: + season = 2 + elif ach["version"] >= 4: + season = 3 + + if type(ach["data"]) is not dict: + ach["data"] = json.loads(ach["data"]) + + sql = Insert(stageup).values( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + + conflict = sql.on_duplicate_key_update( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca achievement for user {ach['user']}") + + else: + self.logger.info(f"Wacca not found, skipping...") From 3f40e083ce7687f0592d85bee4c632e52fd475ff Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 21 Feb 2023 16:46:43 -0500 Subject: [PATCH 021/589] add mucha config to coreconfig --- core/config.py | 1 + core/mucha.py | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/config.py b/core/config.py index f8f57e4..04ad280 100644 --- a/core/config.py +++ b/core/config.py @@ -194,6 +194,7 @@ class CoreConfig(dict): self.allnet = AllnetConfig(self) self.billing = BillingConfig(self) self.aimedb = AimedbConfig(self) + self.mucha = MuchaConfig(self) @classmethod def str_to_loglevel(cls, level_str: str): diff --git a/core/mucha.py b/core/mucha.py index 312d83f..0848c70 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -35,9 +35,14 @@ class MuchaServlet: return b"" req = MuchaAuthRequest(req_dict) - self.logger.info(f"Mucha request {vars(req)}") - resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") - self.logger.info(f"Mucha response {vars(resp)}") + self.logger.debug(f"Mucha request {vars(req)}") + + 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}") + + self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) @@ -48,9 +53,14 @@ class MuchaServlet: return b"" req = MuchaUpdateRequest(req_dict) - self.logger.info(f"Mucha request {vars(req)}") - resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") - self.logger.info(f"Mucha response {vars(resp)}") + self.logger.debug(f"Mucha request {vars(req)}") + + if self.config.server.is_develop: + resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") + else: + resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}") + + self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) From 9c62ea24beeca029fe08645df8314c25f62d0fa8 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 22 Feb 2023 09:59:31 -0500 Subject: [PATCH 022/589] add protobuf to requirements, fixes #2 --- requirements.txt | 1 + requirements_win.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 70b4215..a2529f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ wacky Routes bcrypt jinja2 +protobuf diff --git a/requirements_win.txt b/requirements_win.txt index d36e884..a584c6d 100644 --- a/requirements_win.txt +++ b/requirements_win.txt @@ -14,3 +14,4 @@ wacky Routes bcrypt jinja2 +protobuf From 83f09e180e31bae474068a16b7341a4ecf6446d0 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 22 Feb 2023 09:59:31 -0500 Subject: [PATCH 023/589] add protobuf to requirements, fixes #2 --- requirements.txt | 1 + requirements_win.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 40dbf84..2631c24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ coloredlogs pylibmc wacky Routes +protobuf diff --git a/requirements_win.txt b/requirements_win.txt index 89527fe..cbdbcde 100644 --- a/requirements_win.txt +++ b/requirements_win.txt @@ -12,3 +12,4 @@ inflection coloredlogs wacky Routes +protobuf From fff7eb46660a1b880f8014ccc97dae0bf509e907 Mon Sep 17 00:00:00 2001 From: God601 Date: Wed, 22 Feb 2023 11:47:32 -0500 Subject: [PATCH 024/589] fixing logs for the read.py - Thanks to Dniel97 --- read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.py b/read.py index ffc4812..cec8634 100644 --- a/read.py +++ b/read.py @@ -85,7 +85,7 @@ if __name__ == "__main__": log_fmt = logging.Formatter(log_fmt_str) logger = logging.getLogger("reader") - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(config.server.logs, "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() From b300bb302b63b828239f2240cc526f577dd5569d Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 22 Feb 2023 12:20:50 -0500 Subject: [PATCH 025/589] title fix for the version int - Thanks to Dniel97 --- core/title.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/title.py b/core/title.py index dd47ec8..6c54450 100644 --- a/core/title.py +++ b/core/title.py @@ -71,4 +71,4 @@ class TitleServlet(): self.logger.warn(f"{code} does not dispatch POST") return b"" - return index.render_POST(request, endpoints["version"], endpoints["endpoint"]) + return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"]) From c3aac4c38ec04911b9d74ccbf4eda7a7203fb3cd Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 22 Feb 2023 22:22:03 -0500 Subject: [PATCH 026/589] Move wacca from megaime develop branch, should at least partially fix #3 #4 and #5 --- titles/wacca/base.py | 297 ++++++++++----------------- titles/wacca/handlers/advertise.py | 24 ++- titles/wacca/handlers/helpers.py | 101 ++++----- titles/wacca/handlers/housing.py | 14 +- titles/wacca/handlers/user_info.py | 27 ++- titles/wacca/handlers/user_misc.py | 2 +- titles/wacca/handlers/user_music.py | 25 ++- titles/wacca/handlers/user_status.py | 26 +-- titles/wacca/handlers/user_trial.py | 17 +- titles/wacca/lily.py | 70 ++++--- titles/wacca/lilyr.py | 4 +- titles/wacca/reverse.py | 41 ++-- titles/wacca/s.py | 2 +- 13 files changed, 309 insertions(+), 341 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 598f1fc..815a841 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -77,9 +77,13 @@ class WaccaBase(): 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() def handle_housing_start_request(self, data: Dict) -> Dict: - req = HousingStartRequest(data) + req = HousingStartRequestV1(data) resp = HousingStartResponseV1( 1, @@ -103,7 +107,69 @@ class WaccaBase(): self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_get_request(self, data: Dict)-> Dict: + req = UserStatusGetRequest(data) + resp = UserStatusGetV1Response() + ver_split = req.appVersion.split(".") + + profile = self.data.profile.get_profile(aime_id=req.aimeId) + if profile is None: + self.logger.info(f"No user exists for aime id {req.aimeId}") + resp.profileStatus = ProfileStatus.ProfileRegister + return resp.make() + + + self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") + if profile["last_game_ver"] is None: + profile_ver_split = ver_split + resp.lastGameVersion = req.appVersion + else: + profile_ver_split = profile["last_game_ver"].split(".") + resp.lastGameVersion = profile["last_game_ver"] + + resp.userStatus.userId = profile["id"] + resp.userStatus.username = profile["username"] + resp.userStatus.xp = profile["xp"] + resp.userStatus.danLevel = profile["dan_level"] + resp.userStatus.danType = profile["dan_type"] + 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"]) + 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"]) + if set_icon_id is None: + set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] + resp.setIconId = set_icon_id + + + if int(ver_split[0]) > int(profile_ver_split[0]): + resp.versionStatus = PlayVersionStatus.VersionUpgrade + + elif int(ver_split[0]) < int(profile_ver_split[0]): + resp.versionStatus = PlayVersionStatus.VersionTooNew + + else: + if int(ver_split[1]) > int(profile_ver_split[1]): + resp.versionStatus = PlayVersionStatus.VersionUpgrade + + elif int(ver_split[1]) < int(profile_ver_split[1]): + resp.versionStatus = PlayVersionStatus.VersionTooNew + + else: + if int(ver_split[2]) > int(profile_ver_split[2]): + resp.versionStatus = PlayVersionStatus.VersionUpgrade + + + elif int(ver_split[2]) < int(profile_ver_split[2]): + resp.versionStatus = PlayVersionStatus.VersionTooNew + + return resp.make() + + def handle_user_status_login_request(self, data: Dict)-> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() is_new_day = False @@ -139,120 +205,8 @@ class WaccaBase(): resp.firstLoginDaily = int(is_new_day) return resp.make() - - def handle_user_status_get_request(self, data: Dict) -> List[Any]: - req = UserStatusGetRequest(data) - resp = UserStatusGetV1Response() - ver_split = req.appVersion.split(".") - - profile = self.data.profile.get_profile(aime_id=req.aimeId) - if profile is None: - self.logger.info(f"No user exists for aime id {req.aimeId}") - return resp.make() - - - self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") - if profile["last_game_ver"] is None: - profile_ver_split = ver_split - resp.lastGameVersion = req.appVersion - else: - profile_ver_split = profile["last_game_ver"].split(".") - resp.lastGameVersion = profile["last_game_ver"] - - resp.userStatus.userId = profile["id"] - resp.userStatus.username = profile["username"] - resp.userStatus.xp = profile["xp"] - resp.userStatus.danLevel = profile["dan_level"] - resp.userStatus.danType = profile["dan_type"] - resp.userStatus.wp = profile["wp"] - resp.userStatus.useCount = profile["login_count"] - resp.userStatus.loginDays = profile["login_count_days"] - resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] - - set_title_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) - 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"]) - if set_icon_id is None: - set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] - resp.setIconId = set_icon_id - - if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): - resp.userStatus.loginConsecutiveDays = 0 - - if int(ver_split[0]) > int(profile_ver_split[0]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[0]) < int(profile_ver_split[0]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[1]) > int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[1]) < int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[2]) > int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - - elif int(ver_split[2]) < int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - if profile["always_vip"]: - resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp()) - - elif profile["vip_expire_time"] is not None: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - - return resp.make() - - def handle_user_status_login_request(self, data: Dict) -> List[Any]: - req = UserStatusLoginRequest(data) - resp = UserStatusLoginResponseV2() - is_new_day = False - is_consec_day = False - is_consec_day = True - - if req.userId == 0: - self.logger.info(f"Guest login on {req.chipId}") - resp.lastLoginDate = 0 - - else: - profile = self.data.profile.get_profile(req.userId) - if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") - return resp.make() - - self.logger.info(f"User {req.userId} login on {req.chipId}") - last_login_time = int(profile["last_login_date"].timestamp()) - resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): - is_new_day = True - is_consec_day = True - - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): - is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) - resp.vipInfo.pageYear = datetime.now().year - resp.vipInfo.pageMonth = datetime.now().month - resp.vipInfo.pageDay = datetime.now().day - resp.vipInfo.numItem = 1 - - resp.firstLoginDaily = int(is_new_day) - - return resp.make() - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + def handle_user_status_create_request(self, data: Dict)-> Dict: req = UserStatusCreateRequest(data) profileId = self.data.profile.create_profile(req.aimeId, req.username, self.version) @@ -284,7 +238,7 @@ class WaccaBase(): return UserStatusCreateResponseV2(profileId, req.username).make() - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + def handle_user_status_getDetail_request(self, data: Dict)-> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() @@ -301,16 +255,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) - - if profile["vip_expire_time"] is None: - resp.userStatus.vipExpireTime = 0 - - else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - - if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) + profile_tickets = self.data.item.get_tickets(user_id) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) resp.songPlayStatus = [profile["last_song_id"], 1] @@ -322,8 +267,6 @@ class WaccaBase(): resp.userStatus.danType = profile["dan_type"] resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] - resp.userStatus.loginDays = profile["login_count_days"] - resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 @@ -346,13 +289,9 @@ class WaccaBase(): for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV1(unlock["song_id"], x)) - empty_scores = len(resp.scores) for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) clear_cts = SongDetailClearCounts( song["play_ct"], @@ -368,24 +307,16 @@ class WaccaBase(): song["grade_master_ct"] ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] + deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) for trophy in profile_trophies: resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) @@ -434,7 +365,7 @@ class WaccaBase(): return resp.make() - def handle_user_trial_get_request(self, data: Dict) -> List[Any]: + def handle_user_trial_get_request(self, data: Dict)-> Dict: req = UserTrialGetRequest(data) resp = UserTrialGetResponse() @@ -444,10 +375,6 @@ class WaccaBase(): return resp.make() self.logger.info(f"Get trial info for user {req.profileId}") - - for d in self.allowed_stages: - if d[1] > 0 and d[1] < 10: - resp.stageList.append(StageInfo(d[0], d[1])) stages = self.data.score.get_stageup(user_id, self.version) if stages is None: @@ -474,7 +401,7 @@ class WaccaBase(): return resp.make() - def handle_user_trial_update_request(self, data: Dict) -> List[Any]: + def handle_user_trial_update_request(self, data: Dict)-> Dict: req = UserTrialUpdateRequest(data) total_score = 0 @@ -496,8 +423,8 @@ class WaccaBase(): # 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"]): best_score1 = req.songScores[0] - best_score2 = req.songScores[2] - best_score3 = req.songScores[3] + best_score2 = req.songScores[1] + best_score3 = req.songScores[2] else: best_score1 = old_stage["song1_score"] best_score2 = old_stage["song2_score"] @@ -528,9 +455,9 @@ class WaccaBase(): 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() + return BaseResponse().make() - def handle_user_sugoroku_update_request(self, data: Dict) -> List[Any]: + def handle_user_sugoroku_update_request(self, data: Dict)-> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() @@ -552,10 +479,10 @@ class WaccaBase(): self.data.profile.update_gate(user_id, req.gateId, req.page, req.progress, req.loops, mission_flg, req.totalPts) return resp.make() - def handle_user_info_getMyroom_request(self, data: Dict) -> List[Any]: - return UserInfogetMyroomResponse().make() + def handle_user_info_getMyroom_request(self, data: Dict)-> Dict: + return UserInfogetMyroomResponseV1().make() - def handle_user_music_unlock_request(self, data: Dict) -> List[Any]: + def handle_user_music_unlock_request(self, data: Dict)-> Dict: req = UserMusicUnlockRequest(data) profile = self.data.profile.get_profile(req.profileId) @@ -605,29 +532,35 @@ class WaccaBase(): return UserMusicUnlockResponse(current_wp, new_tickets).make() - def handle_user_info_getRanking_request(self, data: Dict) -> List[Any]: + def handle_user_info_getRanking_request(self, data: Dict)-> Dict: # total score, high score by song, cumulative socre, stage up score, other score, WP ranking # This likely requies calculating standings at regular intervals and caching the results return UserInfogetRankingResponse().make() - def handle_user_music_update_request(self, data: Dict) -> List[Any]: - req = UserMusicUpdateRequest(data) - ver_split = req.appVersion.split(".") + def handle_user_music_update_request(self, data: Dict)-> Dict: + ver_split = data["appVersion"].split(".") if int(ver_split[0]) >= 3: resp = UserMusicUpdateResponseV3() + req = UserMusicUpdateRequestV2(data) elif int(ver_split[0]) >= 2: resp = UserMusicUpdateResponseV2() + req = UserMusicUpdateRequestV2(data) else: resp = UserMusicUpdateResponseV1() + req = UserMusicUpdateRequestV1(data) resp.songDetail.songId = req.songDetail.songId resp.songDetail.difficulty = req.songDetail.difficulty + + if req.profileId == 0: + self.logger.info(f"Guest score for song {req.songDetail.songId} difficulty {req.songDetail.difficulty}") + return resp.make() profile = self.data.profile.get_profile(req.profileId) if profile is None: self.logger.warn(f"handle_user_music_update_request: No profile for game_id {req.profileId}") - return BaseResponse().make() + return resp.make() user_id = profile["user"] self.util_put_items(req.profileId, user_id, req.itemsObtained) @@ -715,18 +648,18 @@ class WaccaBase(): return resp.make() #TODO: Coop and vs data - def handle_user_music_updateCoop_request(self, data: Dict) -> List[Any]: + def handle_user_music_updateCoop_request(self, data: Dict)-> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateVersus_request(self, data: Dict) -> List[Any]: + def handle_user_music_updateVersus_request(self, data: Dict)-> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateTrial_request(self, data: Dict) -> List[Any]: + def handle_user_music_updateTrial_request(self, data: Dict)-> Dict: return self.handle_user_music_update_request(data) - def handle_user_mission_update_request(self, data: Dict) -> List[Any]: + def handle_user_mission_update_request(self, data: Dict)-> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] @@ -742,7 +675,7 @@ class WaccaBase(): return BaseResponse().make() - def handle_user_goods_purchase_request(self, data: Dict) -> List[Any]: + def handle_user_goods_purchase_request(self, data: Dict)-> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() @@ -775,13 +708,13 @@ class WaccaBase(): return resp.make() - def handle_competition_status_login_request(self, data: Dict) -> List[Any]: + def handle_competition_status_login_request(self, data: Dict)-> Dict: return BaseResponse().make() - def handle_competition_status_update_request(self, data: Dict) -> List[Any]: + def handle_competition_status_update_request(self, data: Dict)-> Dict: return BaseResponse().make() - def handle_user_rating_update_request(self, data: Dict) -> List[Any]: + def handle_user_rating_update_request(self, data: Dict)-> Dict: req = UserRatingUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -797,8 +730,8 @@ class WaccaBase(): return BaseResponse().make() - def handle_user_status_update_request(self, data: Dict) -> List[Any]: - req = UserStatusUpdateRequestV2(data) + def handle_user_status_update_request(self, data: Dict)-> Dict: + req = UserStatusUpdateRequestV1(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: @@ -807,8 +740,6 @@ class WaccaBase(): self.util_put_items(req.profileId, user_id, req.itemsRecieved) self.data.profile.update_profile_playtype(req.profileId, req.playType.value, data["appVersion"][:7]) - self.data.profile.update_profile_lastplayed(req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff, - req.lastSongInfo.lastFolderOrd, req.lastSongInfo.lastFolderId, req.lastSongInfo.lastSongOrd) current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"]) current_nav = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_nav_id"]) @@ -826,7 +757,7 @@ class WaccaBase(): self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav) return BaseResponse().make() - def handle_user_info_update_request(self, data: Dict) -> List[Any]: + def handle_user_info_update_request(self, data: Dict)-> Dict: req = UserInfoUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -845,7 +776,7 @@ class WaccaBase(): return BaseResponse().make() - def handle_user_vip_get_request(self, data: Dict) -> List[Any]: + def handle_user_vip_get_request(self, data: Dict)-> Dict: req = UserVipGetRequest(data) resp = UserVipGetResponse() @@ -868,7 +799,7 @@ class WaccaBase(): return resp.make() - def handle_user_vip_start_request(self, data: Dict) -> List[Any]: + def handle_user_vip_start_request(self, data: Dict)-> Dict: req = UserVipStartRequest(data) profile = self.data.profile.get_profile(req.profileId) diff --git a/titles/wacca/handlers/advertise.py b/titles/wacca/handlers/advertise.py index cf41359..a0d8d90 100644 --- a/titles/wacca/handlers/advertise.py +++ b/titles/wacca/handlers/advertise.py @@ -1,6 +1,6 @@ from typing import List, Dict -from titles.wacca.handlers.base import BaseResponse +from titles.wacca.handlers.base import BaseResponse, BaseRequest from titles.wacca.handlers.helpers import Notice # ---advertise/GetNews--- @@ -33,13 +33,33 @@ class GetNewsResponseV1(BaseResponse): class GetNewsResponseV2(GetNewsResponseV1): stoppedProducts: list[int] = [] + + def make(self) -> Dict: + super().make() + self.params.append(self.stoppedProducts) + + return super(GetNewsResponseV1, self).make() + +class GetNewsResponseV3(GetNewsResponseV2): stoppedNavs: list[int] = [] stoppedNavVoices: list[int] = [] def make(self) -> Dict: super().make() - self.params.append(self.stoppedProducts) 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 diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 19991e5..f476216 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any +from typing import List, Optional, Any from enum import Enum from titles.wacca.const import WaccaConstants @@ -41,9 +41,6 @@ class Notice(): int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline] class UserOption(): - opt_id: int - opt_val: Any - def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: self.opt_id = opt_id self.opt_val = opt_val @@ -53,7 +50,7 @@ class UserOption(): class UserStatusV1(): def __init__(self) -> None: - self.userId: int = -1 + self.userId: int = 0 self.username: str = "" self.userType: int = 1 self.xp: int = 0 @@ -62,10 +59,6 @@ class UserStatusV1(): self.wp: int = 0 self.titlePartIds: List[int] = [0, 0, 0] self.useCount: int = 0 - self.loginDays: int = 0 - self.loginConsecutive: int = 0 - self.loginConsecutiveDays: int = 0 - self.vipExpireTime: int = 0 def make(self) -> List: return [ @@ -78,21 +71,25 @@ class UserStatusV1(): self.wp, self.titlePartIds, self.useCount, - self.loginDays, - self.loginConsecutive, - self.loginConsecutiveDays, - self.vipExpireTime ] class UserStatusV2(UserStatusV1): def __init__(self) -> None: - super().__init__() + 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) + ret.append(self.vipExpireTime) ret.append(self.loginsToday) ret.append(self.rating) @@ -336,7 +333,7 @@ class UserItemInfoV3(UserItemInfoV2): class SongDetailClearCounts(): def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0, - am_ct: int = 0, counts: List[int] = None) -> None: + am_ct: int = 0, counts: Optional[List[int]] = None) -> None: if counts is None: self.playCt = play_ct self.clearCt = clear_ct @@ -367,7 +364,7 @@ class SongDetailGradeCountsV1(): masterCt: int def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, counts: List[int] = None) -> None: + 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 @@ -401,7 +398,7 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): ssspCt: int def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, sp: int = 0, ssp: int = 0, sssp: int = 0, counts: List[int] = None, ) -> None: + 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 @@ -464,25 +461,8 @@ class SongUpdateJudgementCounts(): def make(self) -> List: return [self.marvCt, self.greatCt, self.goodCt, self.missCt] -class SongUpdateDetail(): - songId: int - difficulty: int - level: float - score: int - judgements: SongUpdateJudgementCounts - maxCombo: int - grade: WaccaConstants.GRADES - flagCleared: bool - flagMissless: bool - flagFullcombo: bool - flagAllMarvelous: bool - flagGiveUp: bool - skillPt: int - fastCt: int - slowCt: int - flagNewRecord: bool - - def __init__(self, data: List = None) -> None: +class SongUpdateDetailV1(): + def __init__(self, data: List) -> None: if data is not None: self.songId = data[0] self.difficulty = data[1] @@ -498,8 +478,15 @@ class SongUpdateDetail(): self.flagFullcombo = False if data[9] == 0 else True self.flagAllMarvelous = False if data[10] == 0 else True self.flagGiveUp = False if data[11] == 0 else True + self.skillPt = data[12] + self.fastCt = 0 + self.slowCt = 0 + self.flagNewRecord = False - self.skillPt = data[12] +class SongUpdateDetailV2(SongUpdateDetailV1): + def __init__(self, data: List) -> None: + super().__init__(data) + if data is not None: self.fastCt = data[13] self.slowCt = data[14] self.flagNewRecord = False if data[15] == 0 else True @@ -583,7 +570,7 @@ class GateDetailV2(GateDetailV1): return super().make() + [self.missionFlg] class GachaInfo(): - def make() -> List: + def make(self) -> List: return [] class LastSongDetail(): @@ -609,17 +596,6 @@ class FriendDetail(): def make(self) -> List: return [] -class UserOption(): - id = 1 - val = 1 - - def __init__(self, id: int = 1, val: int = val) -> None: - self.id = id - self.val = val - - def make(self) -> List: - return [self.id, self.val] - class LoginBonusInfo(): def __init__(self) -> None: self.tickets: List[TicketItem] = [] @@ -682,19 +658,6 @@ class PlayType(Enum): PlayTypeCoop = 3 PlayTypeStageup = 4 -class SongRatingUpdate(): - song_id = 0 - difficulty = 0 - rating = 0 - - def __init__(self, song: int = 0, difficulty: int = 0, rating: int = 0) -> None: - self.song_id = song - self.difficulty = difficulty - self.rating = rating - - def make(self) -> List: - return [self.song_id, self.difficulty, self.rating] - class StageInfo(): danId: int = 0 danLevel: int = 0 @@ -740,7 +703,6 @@ class MusicUpdateDetailV1(): self.lowestMissCount = 0 self.maxSkillPts = 0 self.locked = 0 - self.rating = 0 def make(self) -> List: return [ @@ -753,10 +715,17 @@ class MusicUpdateDetailV1(): self.lowestMissCount, self.maxSkillPts, self.locked, - self.rating ] 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() diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 7806632..f49f1c2 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -15,12 +15,22 @@ class HousingGetResponse(BaseResponse): return super().make() # ---housing/start---- -class HousingStartRequest(BaseRequest): +class HousingStartRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.unknown0: str = self.params[0] self.errorLog: str = self.params[1] - self.unknown2: str = self.params[2] + self.info: List[HousingInfo] = [] + + for info in self.params[2]: + self.info.append(HousingInfo(info[0], info[1])) + +class HousingStartRequestV2(HousingStartRequestV1): + def __init__(self, data: Dict) -> None: + super(HousingStartRequestV1, self).__init__(data) + self.unknown0: str = self.params[0] + self.errorLog: str = self.params[1] + self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] for info in self.params[3]: diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index c7336d7..6498488 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -23,14 +23,37 @@ class UserInfogetMyroomRequest(BaseRequest): super().__init__(data) self.game_id = int(self.params[0]) -class UserInfogetMyroomResponse(BaseResponse): +class UserInfogetMyroomResponseV1(BaseResponse): + def __init__(self) -> None: + super().__init__() + self.titleViewBadge = 0 + self.iconViewBadge = 0 + self.trophyViewBadge = 0 + self.noteColorViewBadge = 0 + self.noteSoundViewBadge = 0 + self.userViewingInfo = [] + def make(self) -> Dict: self.params = [ - 0,0,0,0,0,[],0,0,0 + self.titleViewBadge, + self.iconViewBadge, + self.trophyViewBadge, + self.noteColorViewBadge, + self.noteSoundViewBadge, + self.userViewingInfo, ] return super().make() +class UserInfogetMyroomResponseV2(UserInfogetMyroomResponseV1): + def __init__(self) -> None: + super().__init__() + + def make(self) -> Dict: + super().make() + self.params += [0, 0, 0] + return super(UserInfogetMyroomResponseV1, self).make() + # ---user/info/getRanking--- class UserInfogetRankingRequest(BaseRequest): game_id = 0 diff --git a/titles/wacca/handlers/user_misc.py b/titles/wacca/handlers/user_misc.py index e710a8b..4dea019 100644 --- a/titles/wacca/handlers/user_misc.py +++ b/titles/wacca/handlers/user_misc.py @@ -25,7 +25,7 @@ class UserGoodsPurchaseResponse(BaseResponse): for ticket in tickets: self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) - def make(self) -> List: + def make(self) -> Dict: tix = [] for ticket in self.tickets: tix.append(ticket.make()) diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index adb11b6..deeda3d 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -1,22 +1,28 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import GenericItemRecv, SongUpdateDetail, TicketItem -from titles.wacca.handlers.helpers import MusicUpdateDetailV1, MusicUpdateDetailV2 -from titles.wacca.handlers.helpers import SeasonalInfoV2, SeasonalInfoV1 +from titles.wacca.handlers.helpers import GenericItemRecv, SongUpdateDetailV2, TicketItem +from titles.wacca.handlers.helpers import MusicUpdateDetailV2, MusicUpdateDetailV3 +from titles.wacca.handlers.helpers import SeasonalInfoV2, SeasonalInfoV1, SongUpdateDetailV1 +from titles.wacca.handlers.helpers import MusicUpdateDetailV1 # ---user/music/update--- -class UserMusicUpdateRequest(BaseRequest): +class UserMusicUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId: int = self.params[0] self.songNumber: int = self.params[1] - self.songDetail = SongUpdateDetail(self.params[2]) + self.songDetail = SongUpdateDetailV1(self.params[2]) self.itemsObtained: List[GenericItemRecv] = [] for itm in data["params"][3]: self.itemsObtained.append(GenericItemRecv(itm[0], itm[1], itm[2])) +class UserMusicUpdateRequestV2(UserMusicUpdateRequestV1): + def __init__(self, data: Dict) -> None: + super().__init__(data) + self.songDetail = SongUpdateDetailV2(self.params[2]) + class UserMusicUpdateResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() @@ -37,21 +43,22 @@ class UserMusicUpdateResponseV1(BaseResponse): class UserMusicUpdateResponseV2(UserMusicUpdateResponseV1): def __init__(self) -> None: super().__init__() + self.songDetail = MusicUpdateDetailV2() self.seasonInfo = SeasonalInfoV2() class UserMusicUpdateResponseV3(UserMusicUpdateResponseV2): def __init__(self) -> None: super().__init__() - self.songDetail = MusicUpdateDetailV2() + self.songDetail = MusicUpdateDetailV3() # ---user/music/updateCoop--- -class UserMusicUpdateCoopRequest(UserMusicUpdateRequest): +class UserMusicUpdateCoopRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.coopData = self.params[4] # ---user/music/updateVs--- -class UserMusicUpdateVsRequest(UserMusicUpdateRequest): +class UserMusicUpdateVsRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.vsData = self.params[4] @@ -77,7 +84,7 @@ class UserMusicUnlockResponse(BaseResponse): for ticket in tickets_remaining: self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) - def make(self) -> List: + def make(self)-> Dict: tickets = [] for ticket in self.tickets: diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 66d0bae..103f878 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -65,11 +65,11 @@ class UserStatusGetDetailResponseV1(BaseResponse): self.userItems: UserItemInfoV1 = UserItemInfoV1() self.scores: List[BestScoreDetailV1] = [] self.songPlayStatus: List[int] = [0,0] - self.seasonInfo: SeasonalInfoV1 = [] + self.seasonInfo: SeasonalInfoV1 = SeasonalInfoV1() self.playAreaList: List = [ [0],[0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0],[0,0,0,0],[0,0,0,0,0,0,0],[0] ] self.songUpdateTime: int = 0 - def make(self) -> List: + def make(self)-> Dict: opts = [] play_modes = [] scores = [] @@ -97,7 +97,7 @@ class UserStatusGetDetailResponseV1(BaseResponse): return super().make() - def find_score_idx(self, song_id: int, difficulty: int = 1, start_idx: int = 0, stop_idx: int = None) -> Optional[int]: + def find_score_idx(self, song_id: int, difficulty: int = 1, start_idx: int = 0, stop_idx: Optional[int] = None) -> Optional[int]: if stop_idx is None or stop_idx > len(self.scores): stop_idx = len(self.scores) @@ -122,7 +122,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): self.gatchaInfo: List[GachaInfo] = [] self.friendList: List[FriendDetail] = [] - def make(self) -> List: + def make(self)-> Dict: super().make() gates = [] friends = [] @@ -164,7 +164,7 @@ class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3): self.bingoStatus: BingoDetail = BingoDetail(0) self.scores: List[BestScoreDetailV2] = [] - def make(self) -> List: + def make(self)-> Dict: super().make() self.params.append(self.bingoStatus.make()) @@ -187,7 +187,8 @@ class UserStatusLoginResponseV1(BaseResponse): self.firstLoginDaily = is_first_login_daily self.lastLoginDate = last_login_date - def make(self) -> List: + def make(self)-> Dict: + super().make() daily = [] consec = [] other = [] @@ -205,25 +206,24 @@ class UserStatusLoginResponseV1(BaseResponse): return super().make() class UserStatusLoginResponseV2(UserStatusLoginResponseV1): - vipInfo: VipInfo - lastLoginDate: int = 0 - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: super().__init__(is_first_login_daily) self.lastLoginDate = last_login_date self.vipInfo = VipInfo() - def make(self) -> List: + def make(self)-> Dict: super().make() self.params.append(self.vipInfo.make()) self.params.append(self.lastLoginDate) return super(UserStatusLoginResponseV1, self).make() class UserStatusLoginResponseV3(UserStatusLoginResponseV2): - unk: List = [] + def __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) -> List: + def make(self)-> Dict: super().make() self.params.append(self.unk) return super(UserStatusLoginResponseV1, self).make() @@ -242,7 +242,7 @@ class UserStatusCreateResponseV1(BaseResponse): self.userStatus.userId = userId self.userStatus.username = username - def make(self) -> List: + def make(self)-> Dict: self.params = [ self.userStatus.make() ] diff --git a/titles/wacca/handlers/user_trial.py b/titles/wacca/handlers/user_trial.py index 84bd44a..6fb75a8 100644 --- a/titles/wacca/handlers/user_trial.py +++ b/titles/wacca/handlers/user_trial.py @@ -1,6 +1,6 @@ from typing import Dict, List from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import StageInfo, StageupClearType +from titles.wacca.handlers.helpers import StageInfo, StageupClearType, GenericItemRecv # --user/trial/get-- class UserTrialGetRequest(BaseRequest): @@ -28,15 +28,18 @@ class UserTrialGetResponse(BaseResponse): class UserTrialUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) - self.profileId = self.params[0] - self.stageId = self.params[1] - self.stageLevel = self.params[2] + self.profileId: int = self.params[0] + self.stageId: int = self.params[1] + self.stageLevel: int = self.params[2] self.clearType = StageupClearType(self.params[3]) - self.songScores = self.params[4] - self.numSongsCleared = self.params[5] - self.itemsObtained = self.params[6] + self.songScores: List[int] = self.params[4] + self.numSongsCleared: int = self.params[5] + self.itemsObtained: List[GenericItemRecv] = [] self.unk7: List = [] + for x in self.params[6]: + self.itemsObtained.append(GenericItemRecv(x[0], x[1], x[2])) + if len(self.params) == 8: self.unk7 = self.params[7] diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 7f9580f..cf5fd43 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -35,8 +35,28 @@ class WaccaLily(WaccaS): (210002, 0), (210003, 0), ] + + def handle_advertise_GetNews_request(self, data: Dict)-> Dict: + resp = GetNewsResponseV3() + return resp.make() - def handle_user_status_get_request(self, data: Dict) -> List[Any]: + def handle_housing_start_request(self, data: Dict) -> Dict: + req = HousingStartRequestV2(data) + + resp = HousingStartResponseV1( + 1, + [ # Recomended songs + 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, + 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, + 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, + 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, + 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, + 1023,1015 + ] + ) + return resp.make() + + def handle_user_status_get_request(self, data: Dict)-> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV2Response() ver_split = req.appVersion.split(".") @@ -115,7 +135,7 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_login_request(self, data: Dict)-> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() is_new_day = False @@ -156,7 +176,7 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + def handle_user_status_getDetail_request(self, data: Dict)-> Dict: req = UserStatusGetDetailRequest(data) ver_split = req.appVersion.split(".") if int(ver_split[1]) >= 53: @@ -255,13 +275,9 @@ class WaccaLily(WaccaS): for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV1(unlock["song_id"], x)) - empty_scores = len(resp.scores) for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) clear_cts = SongDetailClearCounts( song["play_ct"], @@ -277,24 +293,16 @@ class WaccaLily(WaccaS): song["grade_master_ct"] ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] + deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) for trophy in profile_trophies: resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) @@ -348,4 +356,14 @@ class WaccaLily(WaccaS): resp.seasonInfo.noteSoundsObtained = len(resp.userItems.noteSounds) resp.seasonInfo.platesObtained = len(resp.userItems.plates) - return resp.make() \ No newline at end of file + return resp.make() + + def handle_user_info_getMyroom_request(self, data: Dict)-> Dict: + return UserInfogetMyroomResponseV2().make() + + def handle_user_status_update_request(self, data: Dict)-> Dict: + super().handle_user_status_update_request(data) + req = UserStatusUpdateRequestV2(data) + self.data.profile.update_profile_lastplayed(req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff, + req.lastSongInfo.lastFolderOrd, req.lastSongInfo.lastFolderId, req.lastSongInfo.lastSongOrd) + return BaseResponse().make() \ No newline at end of file diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index a359725..559c61d 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -35,7 +35,7 @@ class WaccaLilyR(WaccaLily): (210003, 0), ] - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + def handle_user_status_create_request(self, data: Dict)-> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) @@ -50,5 +50,5 @@ class WaccaLilyR(WaccaLily): return resp - def handle_user_status_logout_request(self, data: Dict) -> List[Any]: + def handle_user_status_logout_request(self, data: Dict)-> Dict: return BaseResponse().make() diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 3d2b571..78db7e4 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -46,12 +46,12 @@ class WaccaReverse(WaccaLilyR): (310006, 0), ] - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_login_request(self, data: Dict)-> Dict: resp = super().handle_user_status_login_request(data) resp["params"].append([]) return resp - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + def handle_user_status_getDetail_request(self, data: Dict)-> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV4() @@ -148,13 +148,9 @@ class WaccaReverse(WaccaLilyR): for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV2(unlock["song_id"], x)) - empty_scores = len(resp.scores) for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) clear_cts = SongDetailClearCounts( song["play_ct"], @@ -167,28 +163,19 @@ 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_master_ct"], song["grade_sp_ct"], song["grade_ssp_ct"], song["grade_sssp_ct"] ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV2(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] - resp.scores.append(deets) + deets = BestScoreDetailV2(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) for trophy in profile_trophies: resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) @@ -247,7 +234,7 @@ class WaccaReverse(WaccaLilyR): return resp.make() - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + def handle_user_status_create_request(self, data: Dict)-> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) diff --git a/titles/wacca/s.py b/titles/wacca/s.py index f302f44..9f23367 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -30,6 +30,6 @@ class WaccaS(WaccaBase): super().__init__(cfg, game_cfg) self.version = WaccaConstants.VER_WACCA_S - def handle_advertise_GetNews_request(self, data: Dict) -> List[Any]: + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV2() return resp.make() From 7df998a51a372f06428a3235b443e0fa497cca6d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 23 Feb 2023 23:11:43 -0500 Subject: [PATCH 027/589] add naomitest endpoint --- core/allnet.py | 4 ++++ index.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index bd2ecce..0594542 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -234,6 +234,10 @@ class AllnetServlet: self.logger.debug(f"response {vars(resp)}") return resp_str.encode("utf-8") + def handle_naomitest(self, request: Request, _: Dict) -> bytes: + self.logger.info(f"Ping from {request.getClientAddress().host}") + return b"naomi ok" + def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]: ret: List[Dict[str, Any]] = [] for x in kvp: diff --git a/index.py b/index.py index 5344ce1..7f28e82 100644 --- a/index.py +++ b/index.py @@ -22,6 +22,7 @@ class HttpDispatcher(resource.Resource): self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg) + 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'])) @@ -29,8 +30,8 @@ class HttpDispatcher(resource.Resource): 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", requirements=dict(game=R"S...")) - self.map_post.connect("title_post", "/{game}/{version}/{endpoint:.*?}", controller="title", action="render_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_POST(self, request: Request) -> bytes: test = self.map_get.match(request.uri.decode()) From e8e6414b66c400eefe5c86bc57599373e39dfd60 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 24 Feb 2023 10:24:35 -0500 Subject: [PATCH 028/589] Install guide for Windows --- docs/INSTALL_WINDOWS.md | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/INSTALL_WINDOWS.md diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md new file mode 100644 index 0000000..5cf934b --- /dev/null +++ b/docs/INSTALL_WINDOWS.md @@ -0,0 +1,84 @@ +# ARTEMiS - Windows 10/11 Guide +This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment + +# Setup +## Install Python Python 3.9 (recommended) or 3.10 +1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe) +2. Install python-3.9.13-amd64.exe + 1. Select Customize installation + 2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next + 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install + +## Install MySQL 8.0 +1. Download MySQL 8.0 Server : [Link](https://cdn.mysql.com//Downloads/MySQLInstaller/mysql-installer-web-community-8.0.31.0.msi) +2. Install mysql-installer-web-community-8.0.31.0.msi + 1. Click on "Add ..." on the side + 2. Click on the "+" next to MySQL Servers + 3. Make sure MySQL Server 8.0.29 - X64 is under the products to be installed. + 4. Hit Next and Next once installed + 5. Select the configuration type "Development Computer" + 6. Hit Next + 7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next + 8. Enter a root password and then hit Next > + 9. Leave everything under Windows Service as default and hit Next > + 10. Click on Execute and for it to finish and hit Next> and then Finish +3. Open MySQL 8.0 Command Line Client and login as your root user +4. Type those commands to create your user and the database +``` +CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; +CREATE DATABASE aime; +GRANT Alter,Create,Delete,Drop,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +FLUSH PRIVILEGES; +exit; +``` + +## Install Python modules +1. Change your work path to the artemis-master folder using 'cd' and install the requirements: +> pip install -r requirements_win.txt + +## Copy/Rename the folder example_config to config + +## Adjust /config/core.yaml + +1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) + - In case you want to run this only locally, set the following values: +``` +server: + listen_address: 0.0.0.0 +title: + hostname: localhost +``` +2. Adjust the proper MySQL information you created earlier +3. Add the AimeDB key at the bottom of the file +4. If the webui is needed, change the flag from False to True + +## Create the database tables for ARTEMiS +> python dbutils.py create + +## Firewall Adjustements +Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): +> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha, 9000 (TCP) + +## Running the ARTEMiS instance +> python index.py + +# Troubleshooting + +## Game does not connect to ARTEMiS Allnet server +1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened + +## Game does not connect to Title Server +1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname +2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. +3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. + +## Unhandled command under AimeDB +1. Double check your AimeDB key under core.yaml, it is incorrect. + +## AttributeError: module 'collections' has no attribute 'Hashable' +1. This means the pyYAML module is obsolete, simply make sure to update pip and then reinstall the requirements + - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: +``` +python -m pip install --upgrade pip +pip install -r requirements_win.txt +``` From b105418431cf83b4c0c3c8161cff34d4fcc8c1de Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 24 Feb 2023 10:36:13 -0500 Subject: [PATCH 029/589] Install guide for Ubuntu 20.04 --- docs/INSTALL_UBUNTU.md | 129 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/INSTALL_UBUNTU.md diff --git a/docs/INSTALL_UBUNTU.md b/docs/INSTALL_UBUNTU.md new file mode 100644 index 0000000..adef3ea --- /dev/null +++ b/docs/INSTALL_UBUNTU.md @@ -0,0 +1,129 @@ +# ARTEMiS - Ubuntu 20.04 LTS Guide +This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment + +# Setup +## Install memcached module +1. sudo apt-get install memcached +2. Under the file /etc/memcached.conf, please make sure the following parameters are set: + +``` +# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default +# Note that the daemon will grow to this size, but does not start out holding this much +# memory + +-I 128m +-m 1024 +``` + +** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles + +3. Restart memcached using: sudo systemctl restart memcached + +## Install MySQL 5.7 +``` +sudo apt update +sudo apt install wget -y +wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb +sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb +``` + 1. During the first prompt, select Ubuntu Bionic + 2. Select the default option + 3. Select MySQL 5.7 + 4. Select the last option +``` +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 +sudo apt-get update +sudo apt-cache policy mysql-server +sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* +``` + +## Default Configuration for MySQL Server +1. sudo mysql_secure_installation +> Make sure to follow the steps that will be prompted such as changing the mysql root password and such + +2. Test your MySQL Server login by doing the following command : +> mysql -u root -p + +## Create the default ARTEMiS database and user +1. mysql -u root -p +2. Please change the password indicated in the next line for a custom secure one and continue with the next commands + +``` +CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; +CREATE DATABASE aime; +GRANT Alter,Create,Delete,Drop,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +FLUSH PRIVILEGES; +exit; +``` + +3. sudo systemctl restart mysql + +## Install Python modules +``` +sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev +sudo apt install libpython3.8-dev +sudo apt-get install python3-software-properties +sudo apt install python3-pip +sudo pip3 install --upgrade pip testresources +sudo pip3 install --upgrade pip setuptools +sudo apt-get install python3-tk +``` +7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements: +> sudo python3 -m pip install -r requirements.txt + +## Copy/Rename the folder example_config to config + +## Adjust /config/core.yaml +1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) +2. Adjust the proper MySQL information you created earlier +3. Add the AimeDB key at the bottom of the file + +## Create the database tables for ARTEMiS +1. sudo python3 dbutils.py create + +2. If you get "No module named Crypto", run the following command: +``` +sudo pip uninstall crypto +sudo pip uninstall pycrypto +sudo pip install pycrypto +``` + +## Firewall Adjustements +``` +sudo ufw allow 80 +sudo ufw allow 443 +sudo ufw allow 8443 +sudo ufw allow 22345 +sudo ufw allow 8090 +sudo ufw allow 8444 +sudo ufw allow 9000 +``` + +## Running the ARTEMiS instance +1. sudo python3 index.py + +# Troubleshooting + +## Game does not connect to ARTEMiS Allnet server +1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened + +## Game does not connect to Title Server +1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname +2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. +3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. + +## Unhandled command under AimeDB +1. Double check your AimeDB key under core.yaml, it is incorrect. + +## Memcache failed, error 3 +1. Make sure memcached is properly installed and running. You can check the status of the service using the following command: +> sudo systemctl status memcached +2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m +3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file. +``` +# Make memcache optional +try: + has_mc = False +except ModuleNotFoundError: + has_mc = False +``` From 6b265ea866fe50f84b3722b64dc3076c37942730 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 13:27:37 -0500 Subject: [PATCH 030/589] pokken: add ssl_enable --- titles/pokken/config.py | 4 ++++ titles/pokken/index.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 1221c08..4eb07c6 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -16,6 +16,10 @@ class PokkenServerConfig(): def hostname(self) -> str: return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'hostname', default="localhost") + @property + def ssl_enable(self) -> bool: + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_enable', default=False) + @property def port(self) -> int: return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9000) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index d0b3efd..facbd52 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -42,7 +42,7 @@ class PokkenServlet(resource.Resource): def setup(self): if self.game_cfg.server.enable: - if self.core_cfg.server.is_develop: + if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port}"\ f":interface={self.game_cfg.server.hostname}:privateKey={self.game_cfg.server.ssl_key}:"\ f"certKey={self.game_cfg.server.ssl_cert}")\ From e7d73dd257063d0bfb51fb0c44b905a16b8608a5 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 13:34:32 -0500 Subject: [PATCH 031/589] add platform_system to requirements.txt --- requirements.txt | 2 +- requirements_win.txt | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 requirements_win.txt diff --git a/requirements.txt b/requirements.txt index a2529f5..ebb17c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ service_identity PyCryptodome inflection coloredlogs -pylibmc +pylibmc; platform_system != "Windows" wacky Routes bcrypt diff --git a/requirements_win.txt b/requirements_win.txt deleted file mode 100644 index a584c6d..0000000 --- a/requirements_win.txt +++ /dev/null @@ -1,17 +0,0 @@ -mypy -wheel -twisted -pytz -pyyaml -sqlalchemy==1.4.46 -mysqlclient -pyopenssl -service_identity -PyCryptodome -inflection -coloredlogs -wacky -Routes -bcrypt -jinja2 -protobuf From 670747cf4898c1fd1030e58efb57fc16438c43e2 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 13:34:32 -0500 Subject: [PATCH 032/589] add platform_system to requirements.txt --- requirements.txt | 2 +- requirements_win.txt | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 requirements_win.txt diff --git a/requirements.txt b/requirements.txt index 2631c24..0536370 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ service_identity PyCryptodome inflection coloredlogs -pylibmc +pylibmc; platform_system != "Windows" wacky Routes protobuf diff --git a/requirements_win.txt b/requirements_win.txt deleted file mode 100644 index cbdbcde..0000000 --- a/requirements_win.txt +++ /dev/null @@ -1,15 +0,0 @@ -mypy -wheel -twisted -pytz -pyyaml -sqlalchemy==1.4.46 -mysqlclient -pyopenssl -service_identity -PyCryptodome -inflection -coloredlogs -wacky -Routes -protobuf From 2bd980165eff134cf1440ec843f48fb381027471 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 13:38:31 -0500 Subject: [PATCH 033/589] update windows guide --- docs/INSTALL_WINDOWS.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index 5cf934b..2bf2c44 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -34,7 +34,7 @@ exit; ## Install Python modules 1. Change your work path to the artemis-master folder using 'cd' and install the requirements: -> pip install -r requirements_win.txt +> pip install -r requirements.txt ## Copy/Rename the folder example_config to config @@ -76,9 +76,8 @@ Make sure the following ports are open both on your router and local Windows fir 1. Double check your AimeDB key under core.yaml, it is incorrect. ## AttributeError: module 'collections' has no attribute 'Hashable' -1. This means the pyYAML module is obsolete, simply make sure to update pip and then reinstall the requirements +1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: ``` -python -m pip install --upgrade pip -pip install -r requirements_win.txt +pip install -r requirements.txt -U ``` From c213926893587783166c500f4501353b3a664aaa Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 14:07:54 -0500 Subject: [PATCH 034/589] added core logger, allnet resiliancy --- core/allnet.py | 26 ++++++++++++++++++-------- index.py | 46 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 0594542..c938a88 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -95,14 +95,19 @@ class AllnetServlet: def handle_poweron(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: - req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())[0]) + req_dict = self.allnet_req_to_dict(request.content.getvalue()) + if req_dict is None: + raise AllnetRequestException() + + req = AllnetPowerOnRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip: raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}") except AllnetRequestException as e: - self.logger.error(e) + if e.message != "": + self.logger.error(e) return b"" if req.format_ver == 3: @@ -155,14 +160,19 @@ class AllnetServlet: def handle_dlorder(self, request: Request, _: Dict): request_ip = request.getClientAddress().host try: - req = AllnetDownloadOrderRequest(self.allnet_req_to_dict(request.content.getvalue())[0]) + req_dict = self.allnet_req_to_dict(request.content.getvalue()) + if req_dict is None: + raise AllnetRequestException() + + req = AllnetDownloadOrderRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using if not req.game_id or not req.ver or not req.serial: raise AllnetRequestException(f"Bad download request params from {request_ip} - {vars(req)}") except AllnetRequestException as e: - self.logger.error(e) + if e.message != "": + self.logger.error(e) return b"" resp = AllnetDownloadOrderResponse() @@ -265,7 +275,7 @@ class AllnetServlet: return self.kvp_to_dict(sections) except Exception as e: - self.logger.error(e) + self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") return None def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: @@ -280,7 +290,7 @@ class AllnetServlet: return self.kvp_to_dict(sections) except Exception as e: - self.logger.error(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]: @@ -307,7 +317,7 @@ class AllnetServlet: return urlencode except Exception as e: - self.logger.error(e) + self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}") return None class AllnetPowerOnRequest(): @@ -407,6 +417,6 @@ class BillingResponse(): # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period class AllnetRequestException(Exception): - def __init__(self, message="Allnet Request Error") -> None: + def __init__(self, message="") -> None: self.message = message super().__init__(self.message) diff --git a/index.py b/index.py index 7f28e82..da1a11b 100644 --- a/index.py +++ b/index.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import argparse +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler from typing import Dict import yaml from os import path, mkdir, access, W_OK @@ -17,6 +19,7 @@ class HttpDispatcher(resource.Resource): self.isLeaf = True 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) @@ -33,28 +36,36 @@ class HttpDispatcher(resource.Resource): 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_POST(self, request: Request) -> bytes: + def render_GET(self, request: Request) -> bytes: test = self.map_get.match(request.uri.decode()) if test is None: - return b"" + 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: test = self.map_post.match(request.uri.decode()) if test is None: - return b"" + 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: - return b"" + 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: - return b"" + 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") @@ -80,18 +91,37 @@ if __name__ == "__main__": cfg: CoreConfig = CoreConfig() cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + logger = logging.getLogger("core") + if not hasattr(logger, "initialized"): + log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + logger.addHandler(fileHandler) + logger.addHandler(consoleHandler) + + # TODO: Add log level for core to config + logger.setLevel(cfg.allnet.loglevel) + coloredlogs.install(level=cfg.allnet.loglevel, logger=logger, fmt=log_fmt_str) + logger.initialized = True + 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") + logger.error(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions") exit(1) if not cfg.aimedb.key: - print("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") + logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") exit(1) - print(f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode") + 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}" From bd1665a8497cb9412bd8327623aeb8f70350ed7f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 14:10:41 -0500 Subject: [PATCH 035/589] add logging levels for index.py and read.py --- index.py | 6 +++--- read.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/index.py b/index.py index da1a11b..689e9ee 100644 --- a/index.py +++ b/index.py @@ -105,9 +105,9 @@ if __name__ == "__main__": logger.addHandler(fileHandler) logger.addHandler(consoleHandler) - # TODO: Add log level for core to config - logger.setLevel(cfg.allnet.loglevel) - coloredlogs.install(level=cfg.allnet.loglevel, logger=logger, fmt=log_fmt_str) + log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO + logger.setLevel(log_lv) + coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) logger.initialized = True if not path.exists(cfg.server.log_dir): diff --git a/read.py b/read.py index cec8634..869ad97 100644 --- a/read.py +++ b/read.py @@ -94,8 +94,9 @@ if __name__ == "__main__": logger.addHandler(fileHandler) logger.addHandler(consoleHandler) - logger.setLevel(logging.INFO) - coloredlogs.install(level=logging.INFO, logger=logger, fmt=log_fmt_str) + 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) if args.series is None or args.version is None: logger.error("Game or version not specified") From cb227f9cf4ef6e8e2d6ae158f2e86c5f5046cdb0 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 14:13:31 -0500 Subject: [PATCH 036/589] remove unnecessassary print statements --- core/utils.py | 6 +++--- dbutils.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/utils.py b/core/utils.py index a14f9ff..d4b7f16 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ -from typing import Dict, List, Any, Optional +from typing import Dict, Any from types import ModuleType -import zlib, base64 +import logging import importlib from os import walk @@ -17,6 +17,6 @@ class Utils: ret[dir] = mod except ImportError as e: - print(f"{dir} - {e}") + logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") raise return ret diff --git a/dbutils.py b/dbutils.py index 8e6024b..d52128e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -23,22 +23,22 @@ if __name__=='__main__': elif args.action == "upgrade" or args.action == "rollback": if args.version is None: - print("Must set game and version to migrate to") + data.logger.error("Must set game and version to migrate to") exit(0) if args.game is None: - print("No game set, upgrading core schema") + data.logger.info("No game set, upgrading core schema") data.migrate_database("CORE", int(args.version)) else: data.migrate_database(args.game, int(args.version), args.action) elif args.action == "migrate": - print("Migrating from old schema to new schema") + data.logger.info("Migrating from old schema to new schema") data.restore_from_old_schema() elif args.action == "dump": - print("Dumping old schema to migrate to new schema") + data.logger.info("Dumping old schema to migrate to new schema") data.dump_db() elif args.action == "generate": From b31e739ecd7a40a7967fa7b7cac819be544d4989 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 14:14:18 -0500 Subject: [PATCH 037/589] remove unnecessassary logging dup check --- index.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/index.py b/index.py index 689e9ee..e514b10 100644 --- a/index.py +++ b/index.py @@ -92,23 +92,21 @@ if __name__ == "__main__": cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) logger = logging.getLogger("core") - if not hasattr(logger, "initialized"): - log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10) - fileHandler.setFormatter(log_fmt) - - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) - logger.addHandler(fileHandler) - logger.addHandler(consoleHandler) - - log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO - logger.setLevel(log_lv) - coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - logger.initialized = True + 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) From 806dd717e6456a47f569fae2f7a05b3f1dc5d46f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 27 Feb 2023 11:39:42 -0500 Subject: [PATCH 038/589] maidx: fix score, playlog and courses not saving properly --- titles/mai2/schema/score.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 0ef6da4..1b4d65d 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -161,6 +161,7 @@ course = Table( class Mai2ScoreData(BaseData): def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: + score_data["user"] = user_id sql = insert(best_score).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) @@ -197,6 +198,7 @@ class Mai2ScoreData(BaseData): return result.fetchone() def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: + playlog_data["user"] = user_id sql = insert(playlog).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) @@ -208,6 +210,7 @@ class Mai2ScoreData(BaseData): return result.lastrowid def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: + course_data["user"] = user_id sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) From 1f2d12f3185aecdf357f4eafde5695bd88dcb931 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 27 Feb 2023 11:55:51 -0500 Subject: [PATCH 039/589] maidx: upgrade schema for uni+ --- core/data/schema/versions/SDEZ_1_rollback.sql | 3 +++ core/data/schema/versions/SDEZ_2_upgrade.sql | 3 +++ docs/INSTALL_UBUNTU.md | 2 +- docs/INSTALL_WINDOWS.md | 2 +- titles/mai2/__init__.py | 2 +- titles/mai2/schema/score.py | 1 + 6 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_1_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_2_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_1_rollback.sql b/core/data/schema/versions/SDEZ_1_rollback.sql new file mode 100644 index 0000000..6ebbab4 --- /dev/null +++ b/core/data/schema/versions/SDEZ_1_rollback.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE mai2_playlog DROP COLUMN trialPlayAchievement; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_upgrade.sql b/core/data/schema/versions/SDEZ_2_upgrade.sql new file mode 100644 index 0000000..626cc60 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_upgrade.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE mai2_playlog ADD trialPlayAchievement INT NULL; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/docs/INSTALL_UBUNTU.md b/docs/INSTALL_UBUNTU.md index adef3ea..eaabb0c 100644 --- a/docs/INSTALL_UBUNTU.md +++ b/docs/INSTALL_UBUNTU.md @@ -51,7 +51,7 @@ sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5 ``` CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; FLUSH PRIVILEGES; exit; ``` diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index 2bf2c44..abd1e43 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -27,7 +27,7 @@ This step-by-step guide assumes that you are using a fresh install of Windows 10 ``` CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; FLUSH PRIVILEGES; exit; ``` diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 1c51244..3cc5f97 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -15,4 +15,4 @@ trailing_slash = True use_default_host = False host = "" -current_schema_version = 1 \ No newline at end of file +current_schema_version = 2 \ No newline at end of file diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 1b4d65d..59a600c 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -135,6 +135,7 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), + Column("trialPlayAchievement", Integer), mysql_charset='utf8mb4' ) From 0da30534548626c5f0170d973a76684f52da7f31 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 27 Feb 2023 11:57:49 -0500 Subject: [PATCH 040/589] add dummy database attribute for pokken --- titles/pokken/__init__.py | 2 ++ titles/pokken/database.py | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 titles/pokken/database.py diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index 5be8ac6..3b574fd 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -1,7 +1,9 @@ from titles.pokken.index import PokkenServlet from titles.pokken.const import PokkenConstants +from titles.pokken.database import PokkenData index = PokkenServlet +database = PokkenData use_default_title = True include_protocol = True diff --git a/titles/pokken/database.py b/titles/pokken/database.py new file mode 100644 index 0000000..eff928b --- /dev/null +++ b/titles/pokken/database.py @@ -0,0 +1,6 @@ +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 From abe1fa7853568fe4d9396d21c2e466b5c3905b3f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 27 Feb 2023 16:51:17 -0500 Subject: [PATCH 041/589] Pokken: added check for ssl cert/key --- titles/pokken/index.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index facbd52..5180399 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -5,7 +5,7 @@ import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from titles.pokken.proto import jackal_pb2 -from google.protobuf import text_format +from os import path from core.config import CoreConfig from titles.pokken.config import PokkenConfig @@ -42,12 +42,19 @@ class PokkenServlet(resource.Resource): def setup(self): if self.game_cfg.server.enable: - if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: + key_exists = path.exists(self.game_cfg.server.ssl_key) + cert_exists = path.exists(self.game_cfg.server.ssl_cert) + + if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable and key_exists and cert_exists: endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port}"\ f":interface={self.game_cfg.server.hostname}:privateKey={self.game_cfg.server.ssl_key}:"\ f"certKey={self.game_cfg.server.ssl_cert}")\ .listen(server.Site(PokkenServlet(self.core_cfg, self.config_dir))) + elif self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: + 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.") + return + else: endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}"\ f":interface={self.game_cfg.server.hostname}")\ From 066f92d94b7ab517a6d5a53258b9a8a117229be7 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 28 Feb 2023 00:41:32 -0500 Subject: [PATCH 042/589] pokken index fixes --- example_config/pokken.yaml | 2 -- titles/pokken/config.py | 8 ------ titles/pokken/index.py | 55 +++++++++++++++++++++++--------------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 967cca6..5523996 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -1,8 +1,6 @@ server: enable: True loglevel: "info" - hostname: "localhost" - ssl_enable: False port: 9000 port_matching: 9001 ssl_cert: cert/pokken.crt diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 4eb07c6..b6596f2 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -12,14 +12,6 @@ class PokkenServerConfig(): def loglevel(self) -> int: return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'loglevel', default="info")) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'hostname', default="localhost") - - @property - def ssl_enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_enable', default=False) - @property def port(self) -> int: return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9000) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 5180399..a47cac7 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -6,6 +6,7 @@ import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from titles.pokken.proto import jackal_pb2 from os import path +from google.protobuf.message import DecodeError from core.config import CoreConfig from titles.pokken.config import PokkenConfig @@ -41,34 +42,46 @@ class PokkenServlet(resource.Resource): self.base = PokkenBase(core_cfg, self.game_cfg) def setup(self): - if self.game_cfg.server.enable: + """ + 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 self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable and key_exists and cert_exists: - endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port}"\ - f":interface={self.game_cfg.server.hostname}:privateKey={self.game_cfg.server.ssl_key}:"\ - f"certKey={self.game_cfg.server.ssl_cert}")\ - .listen(server.Site(PokkenServlet(self.core_cfg, self.config_dir))) - - elif self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: - 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.") - return - else: - endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}"\ - f":interface={self.game_cfg.server.hostname}")\ - .listen(server.Site(PokkenServlet(self.core_cfg, self.config_dir))) + 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}") - self.logger.info(f"Pokken title server ready on port {self.game_cfg.server.port}") - - def render_POST(self, request: Request, version: int, endpoints: str) -> bytes: - req_url = request.uri.decode() - if req_url == "/matching": + 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 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") + return b"" pokken_request = jackal_pb2.Request() - pokken_request.ParseFromString(request.content.getvalue()) + try: + pokken_request.ParseFromString(content) + except DecodeError as e: + self.logger.warn(f"{e} {content}") + return b"" + endpoint = jackal_pb2.MessageType(pokken_request.type).name.lower() self.logger.info(f"{endpoint} request") From 02848859265ba1d52ff03344b2d1df32876e35dd Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 28 Feb 2023 00:41:43 -0500 Subject: [PATCH 043/589] add pokken, cxb and frontend to nginx config --- example_config/nginx_example.conf | 97 ++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index 3b8bf3c..096a072 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -18,7 +18,7 @@ server { } } -# SSL titles +# SSL titles, comment out if you don't plan on accepting SSL titles server { listen 443 ssl default_server; listen [::]:443 ssl default_server; @@ -57,4 +57,99 @@ server { location / { proxy_pass http://localhost:8444/; } +} + +# Pokken, comment this out if you don't plan on serving pokken. +server { + listen 443 ssl; + server_name pokken.hostname.here; + + ssl_certificate /path/to/cert/pokken.pem; + ssl_certificate_key /path/to/cert/pokken.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=1"; + ssl_prefer_server_ciphers off; + + location / { + proxy_pass http://localhost:8080/; + } +} + +# CXB, comment this out if you don't plan on serving crossbeats. +server { + listen 443 ssl; + server_name cxb.hostname.here; + + ssl_certificate /path/to/cert/cxb.pem; + ssl_certificate_key /path/to/cert/cxb.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=1"; + ssl_prefer_server_ciphers off; + + location / { + proxy_pass http://localhost:8080/SDBT/104/; + } +} + +# CXB, comment this out if you don't plan on serving crossbeats. +server { + listen 443 ssl; + server_name cxb.hostname.here; + + ssl_certificate /path/to/cert/cxb.pem; + ssl_certificate_key /path/to/cert/cxb.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=1"; + ssl_prefer_server_ciphers off; + + location / { + proxy_pass http://localhost:8080/SDBT/104/; + } +} + +# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend +server { + listen 80; + server_name frontend.hostname.here + + location / { + return 301 https://$host$request_uri; + # If you don't want https redirection, comment the line above and uncomment the line below + # proxy_pass http://localhost:8090/; + } +} + +# Frontend HTTPS. Comment out if you on't intend to use the frontend +server { + listen 443 ssl; + + ssl_certificate /path/to/cert/frontend.pem; + ssl_certificate_key /path/to/cert/frontend.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # intermediate configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + location / { + proxy_pass http://localhost:8090/; + } } \ No newline at end of file From 0e3265a162e67fb3688a43424f5c26edc8153359 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 28 Feb 2023 17:32:23 -0500 Subject: [PATCH 044/589] wacca: fix vip_start --- titles/wacca/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 815a841..5bb83ae 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -810,9 +810,9 @@ class WaccaBase(): if "always_vip" in profile and profile["always_vip"] or self.game_config.mods.always_vip: return UserVipStartResponse(int((self.srvtime + timedelta(days=req.days)).timestamp())).make() - profile["vip_expire_time"] = int((self.srvtime + timedelta(days=req.days)).timestamp()) - self.data.profile.update_vip_time(req.profileId, self.srvtime + timedelta(days=req.days)) - return UserVipStartResponse(profile["vip_expire_time"]).make() + vip_exp_time = (self.srvtime + timedelta(days=req.days)) + self.data.profile.update_vip_time(req.profileId, vip_exp_time) + return UserVipStartResponse(int(vip_exp_time.timestamp())).make() def util_put_items(self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv]) -> None: if user_id is None or profile_id <= 0: From b81767af8a80064486a00bd3b0febc9f94bd049f Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 1 Mar 2023 21:09:06 +0100 Subject: [PATCH 045/589] Chunithm New!!+ Importer and settings fixed --- core/data/schema/versions/SDHD_1_rollback.sql | 1 + core/data/schema/versions/SDHD_2_upgrade.sql | 1 + titles/chuni/new.py | 8 ++++---- titles/chuni/newplus.py | 8 ++++---- titles/chuni/read.py | 2 +- titles/chuni/schema/static.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 core/data/schema/versions/SDHD_1_rollback.sql create mode 100644 core/data/schema/versions/SDHD_2_upgrade.sql diff --git a/core/data/schema/versions/SDHD_1_rollback.sql b/core/data/schema/versions/SDHD_1_rollback.sql new file mode 100644 index 0000000..e8bca83 --- /dev/null +++ b/core/data/schema/versions/SDHD_1_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_static_music CHANGE COLUMN worldsEndTag worldsEndTag VARCHAR(20) NULL DEFAULT NULL ; diff --git a/core/data/schema/versions/SDHD_2_upgrade.sql b/core/data/schema/versions/SDHD_2_upgrade.sql new file mode 100644 index 0000000..30f0fc4 --- /dev/null +++ b/core/data/schema/versions/SDHD_2_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_static_music CHANGE COLUMN worldsEndTag worldsEndTag VARCHAR(7) NULL DEFAULT NULL ; diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 909284e..5c21df6 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -47,10 +47,10 @@ class ChuniNew(ChuniBase): "matchErrorLimit": 9999, "romVersion": "2.00.00", "dataVersion": "2.00.00", - "matchingUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "matchingUriX": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "udpHolePunchUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "reflectorUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "matchingUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "matchingUriX": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "udpHolePunchUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "reflectorUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", }, "isDumpUpload": "false", "isAou": "false", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index c048f8a..48de8d0 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -16,8 +16,8 @@ class ChuniNewPlus(ChuniNew): 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.server.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["matchingUriX"] = f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["udpHolePunchUri"] = f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["reflectorUri"] = f"http://{self.core_cfg.server.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"]["matchingUri"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"]["matchingUriX"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"]["udpHolePunchUri"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"]["reflectorUri"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" return ret diff --git a/titles/chuni/read.py b/titles/chuni/read.py index dd67c03..1a666e6 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -82,7 +82,7 @@ class ChuniReader(BaseReader): for MusicFumenData in fumens.findall('MusicFumenData'): fumen_path = MusicFumenData.find('file').find("path") - if fumen_path.text is not None: + if fumen_path is not None: chart_id = MusicFumenData.find('type').find('id').text if chart_id == "4": level = float(xml_root.find("starDifType").text) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 99dd3e8..fbfae11 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -34,7 +34,7 @@ music = Table( Column("level", Float), Column("genre", String(255)), Column("jacketPath", String(255)), - Column("worldsEndTag", String(20)), + Column("worldsEndTag", String(7)), UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"), mysql_charset='utf8mb4' ) From 842e3a313e93eebaf6348c748694a7db1be3042f Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 1 Mar 2023 21:18:29 +0100 Subject: [PATCH 046/589] chuni: use title hostname instead of server hostname --- titles/chuni/new.py | 8 ++++---- titles/chuni/newplus.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 5c21df6..4b5ba2d 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -47,10 +47,10 @@ class ChuniNew(ChuniBase): "matchErrorLimit": 9999, "romVersion": "2.00.00", "dataVersion": "2.00.00", - "matchingUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "matchingUriX": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "udpHolePunchUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", - "reflectorUri": f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "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", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 48de8d0..7ebdc96 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -16,8 +16,8 @@ class ChuniNewPlus(ChuniNew): 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.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["matchingUriX"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["udpHolePunchUri"] = f"http://{self.core_cfg.server.listen_address}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" - ret["gameSetting"]["reflectorUri"] = f"http://{self.core_cfg.server.listen_address}:{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 From 97aeba20e5aab5fabe6bb76ce1b13baffaebea60 Mon Sep 17 00:00:00 2001 From: Raymonf Date: Wed, 1 Mar 2023 16:08:36 -0500 Subject: [PATCH 047/589] chuni: add missing columns for course mode --- core/data/schema/versions/SDHD_2_rollback.sql | 1 + core/data/schema/versions/SDHD_3_upgrade.sql | 1 + titles/chuni/schema/score.py | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 core/data/schema/versions/SDHD_2_rollback.sql create mode 100644 core/data/schema/versions/SDHD_3_upgrade.sql diff --git a/core/data/schema/versions/SDHD_2_rollback.sql b/core/data/schema/versions/SDHD_2_rollback.sql new file mode 100644 index 0000000..de11416 --- /dev/null +++ b/core/data/schema/versions/SDHD_2_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_score_course DROP COLUMN theoryCount, DROP COLUMN orderId, DROP COLUMN playerRating; diff --git a/core/data/schema/versions/SDHD_3_upgrade.sql b/core/data/schema/versions/SDHD_3_upgrade.sql new file mode 100644 index 0000000..57498cc --- /dev/null +++ b/core/data/schema/versions/SDHD_3_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_score_course ADD theoryCount int(11), ADD orderId int(11), ADD playerRating int(11); diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 353401f..81f3212 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -29,6 +29,9 @@ course = Table( Column("param3", Integer), Column("param4", Integer), Column("isClear", Boolean), + Column("theoryCount", Integer), + Column("orderId", Integer), + Column("playerRating", Integer), UniqueConstraint("user", "courseId", name="chuni_score_course_uk"), mysql_charset='utf8mb4' ) From b0bf151c9f868d3fca2eebc3ea43c8dd7941436e Mon Sep 17 00:00:00 2001 From: Raymonf Date: Wed, 1 Mar 2023 20:04:34 -0500 Subject: [PATCH 048/589] chuni: SDHD -> SDBT for upgrade scripts --- .../schema/versions/{SDHD_1_rollback.sql => SDBT_1_rollback.sql} | 0 .../schema/versions/{SDHD_2_rollback.sql => SDBT_2_rollback.sql} | 0 .../schema/versions/{SDHD_2_upgrade.sql => SDBT_2_upgrade.sql} | 0 .../schema/versions/{SDHD_3_upgrade.sql => SDBT_3_upgrade.sql} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename core/data/schema/versions/{SDHD_1_rollback.sql => SDBT_1_rollback.sql} (100%) rename core/data/schema/versions/{SDHD_2_rollback.sql => SDBT_2_rollback.sql} (100%) rename core/data/schema/versions/{SDHD_2_upgrade.sql => SDBT_2_upgrade.sql} (100%) rename core/data/schema/versions/{SDHD_3_upgrade.sql => SDBT_3_upgrade.sql} (100%) diff --git a/core/data/schema/versions/SDHD_1_rollback.sql b/core/data/schema/versions/SDBT_1_rollback.sql similarity index 100% rename from core/data/schema/versions/SDHD_1_rollback.sql rename to core/data/schema/versions/SDBT_1_rollback.sql diff --git a/core/data/schema/versions/SDHD_2_rollback.sql b/core/data/schema/versions/SDBT_2_rollback.sql similarity index 100% rename from core/data/schema/versions/SDHD_2_rollback.sql rename to core/data/schema/versions/SDBT_2_rollback.sql diff --git a/core/data/schema/versions/SDHD_2_upgrade.sql b/core/data/schema/versions/SDBT_2_upgrade.sql similarity index 100% rename from core/data/schema/versions/SDHD_2_upgrade.sql rename to core/data/schema/versions/SDBT_2_upgrade.sql diff --git a/core/data/schema/versions/SDHD_3_upgrade.sql b/core/data/schema/versions/SDBT_3_upgrade.sql similarity index 100% rename from core/data/schema/versions/SDHD_3_upgrade.sql rename to core/data/schema/versions/SDBT_3_upgrade.sql From 88f6eba30bf1c86f53623cfbe0c28b98ed20a0da Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 21:48:43 -0500 Subject: [PATCH 049/589] wacca: add region IDs and version helper classes --- titles/wacca/base.py | 30 ++-------- titles/wacca/const.py | 64 +++++++++++++++++--- titles/wacca/handlers/base.py | 3 +- titles/wacca/handlers/helpers.py | 88 ++++++++++++++++++++++++++++ titles/wacca/handlers/user_status.py | 4 +- titles/wacca/index.py | 7 +-- titles/wacca/lily.py | 35 +++-------- 7 files changed, 165 insertions(+), 66 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 5bb83ae..535048f 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -110,7 +110,6 @@ class WaccaBase(): def handle_user_status_get_request(self, data: Dict)-> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV1Response() - ver_split = req.appVersion.split(".") profile = self.data.profile.get_profile(aime_id=req.aimeId) if profile is None: @@ -118,14 +117,11 @@ class WaccaBase(): resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() - self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: - profile_ver_split = ver_split - resp.lastGameVersion = req.appVersion + resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: - profile_ver_split = profile["last_game_ver"].split(".") - resp.lastGameVersion = profile["last_game_ver"] + resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] @@ -145,27 +141,11 @@ class WaccaBase(): set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] resp.setIconId = set_icon_id - - if int(ver_split[0]) > int(profile_ver_split[0]): + if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[0]) < int(profile_ver_split[0]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - else: - if int(ver_split[1]) > int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[1]) < int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[2]) > int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - - elif int(ver_split[2]) < int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionTooNew + elif req.appVersion < resp.lastGameVersion: + resp.versionStatus = PlayVersionStatus.VersionTooNew return resp.make() diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 910f313..0590f5f 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -95,18 +95,68 @@ class WaccaConstants(): "set_plate_id": 1005, # ID } - DIFFICULTIES = { - "Normal": 1, - "Hard": 2, - "Expert": 3, - "Inferno": 4, - } - class Difficulty(Enum): NORMAL = 1 HARD = 2 EXPERT = 3 INFERNO = 4 + + class Region(Enum): + NONE = 0 + HOKKAIDO = 1 + AOMORI = 2 + IWATE = 3 + MIYAGI = 4 + AKITA = 5 + YAMAGATA = 6 + FUKUSHIMA = 7 + IBARAKI = 8 + TOCHIGI = 9 + GUNMA = 10 + SAITAMA = 11 + CHIBA = 12 + TOKYO = 13 + KANAGAWA = 14 + NIIGATA = 15 + TOYAMA = 16 + ISHIKAWA = 17 + FUKUI = 18 + YAMANASHI = 19 + NAGANO = 20 + GIFU = 21 + SHIZUOKA = 22 + AICHI = 23 + MIE = 24 + SHIGA = 25 + KYOTO = 26 + OSAKA = 27 + HYOGO = 28 + NARA = 29 + WAKAYAMA = 30 + TOTTORI = 31 + SHIMANE = 32 + OKAYAMA = 33 + HIROSHIMA = 34 + YAMAGUCHI = 35 + TOKUSHIMA = 36 + KAGAWA = 37 + EHIME = 38 + KOCHI = 39 + FUKUOKA = 40 + SAGA = 41 + NAGASAKI = 42 + KUMAMOTO = 43 + OITA = 44 + MIYAZAKI = 45 + KAGOSHIMA = 46 + OKINAWA = 47 + UNITED_STATES = 48 + TAIWAN = 49 + HONG_KONG = 50 + SINGAPORE = 51 + KOREA = 52 + + VALID_COUNTRIES = set(["JPN", "USA", "KOR", "HKG", "SGP"]) @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/wacca/handlers/base.py b/titles/wacca/handlers/base.py index 1e1197b..d7a2fb2 100644 --- a/titles/wacca/handlers/base.py +++ b/titles/wacca/handlers/base.py @@ -1,10 +1,11 @@ from typing import Dict, List +from titles.wacca.handlers.helpers import Version from datetime import datetime class BaseRequest(): def __init__(self, data: Dict) -> None: self.requestNo: int = data["requestNo"] - self.appVersion: str = data["appVersion"] + self.appVersion: Version = Version(data["appVersion"]) self.boardId: str = data["boardId"] self.chipId: str = data["chipId"] self.params: List = data["params"] diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index f476216..752a656 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -3,6 +3,94 @@ from enum import Enum from titles.wacca.const import WaccaConstants +class ShortVersion: + def __init__(self, version: str = "", major = 1, minor = 0, patch = 0) -> None: + split = version.split(".") + if len(split) >= 3: + self.major = int(split[0]) + self.minor = int(split[1]) + self.patch = int(split[2]) + + else: + self.major = major + self.minor = minor + self.patch = patch + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}" + + def __int__(self) -> int: + return (self.major * 10000) + (self.minor * 100) + self.patch + + def __eq__(self, other: "ShortVersion"): + return self.major == other.major and self.minor == other.minor and self.patch == other.patch + + def __gt__(self, other: "ShortVersion"): + if self.major > other.major: + return True + elif self.major == other.major: + if self.minor > other.minor: + return True + elif self.minor == other.minor: + if self.patch > other.patch: + return True + + return False + + def __ge__(self, other: "ShortVersion"): + if self.major > other.major: + return True + elif self.major == other.major: + if self.minor > other.minor: + return True + elif self.minor == other.minor: + if self.patch > other.patch or self.patch == other.patch: + return True + + return False + + def __lt__(self, other: "ShortVersion"): + if self.major < other.major: + return True + elif self.major == other.major: + if self.minor < other.minor: + return True + elif self.minor == other.minor: + if self.patch < other.patch: + return True + + return False + + def __le__(self, other: "ShortVersion"): + if self.major < other.major: + return True + elif self.major == other.major: + if self.minor < other.minor: + return True + elif self.minor == other.minor: + if self.patch < other.patch or self.patch == other.patch: + return True + + return False + +class Version(ShortVersion): + def __init__(self, version = "", major = 1, minor = 0, patch = 0, country = "JPN", build = 0, role = "C") -> None: + super().__init__(version, major, minor, patch) + split = version.split(".") + if len(split) >= 6: + self.country = split[3] + self.build = int(split[4]) + self.role = 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 diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 103f878..b852dd1 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -19,7 +19,7 @@ class UserStatusGetV1Response(BaseResponse): self.setIconId: int = 0 self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood - self.lastGameVersion: str = "" + self.lastGameVersion: ShortVersion = ShortVersion() def make(self) -> Dict: self.params = [ @@ -29,7 +29,7 @@ class UserStatusGetV1Response(BaseResponse): self.profileStatus.value, [ self.versionStatus.value, - self.lastGameVersion + str(self.lastGameVersion) ] ] diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 963fa7a..37d3f9e 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -18,6 +18,7 @@ from titles.wacca.lily import WaccaLily from titles.wacca.s import WaccaS from titles.wacca.base import WaccaBase from titles.wacca.handlers.base import BaseResponse +from titles.wacca.handlers.helpers import Version class WaccaServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -55,12 +56,10 @@ 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() - - version_full = [] try: req_json = json.loads(request.content.getvalue()) - version_full = req_json["appVersion"].split(".") + version_full = Version(req_json["appVersion"]) except: self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}") resp = BaseResponse() @@ -76,7 +75,7 @@ class WaccaServlet(): func_to_find += f"{url_split[x + start_req_idx]}_" func_to_find += "request" - ver_search = (int(version_full[0]) * 10000) + (int(version_full[1]) * 100) + int(version_full[2]) + ver_search = int(version_full) if ver_search < 15000: internal_ver = WaccaConstants.VER_WACCA diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index cf5fd43..77d37de 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -59,7 +59,6 @@ class WaccaLily(WaccaS): def handle_user_status_get_request(self, data: Dict)-> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV2Response() - ver_split = req.appVersion.split(".") profile = self.data.profile.get_profile(aime_id=req.aimeId) if profile is None: @@ -69,11 +68,9 @@ class WaccaLily(WaccaS): self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: - profile_ver_split = ver_split - resp.lastGameVersion = req.appVersion + resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: - profile_ver_split = profile["last_game_ver"].split(".") - resp.lastGameVersion = profile["last_game_ver"] + resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] @@ -103,26 +100,11 @@ class WaccaLily(WaccaS): if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): resp.userStatus.loginConsecutiveDays = 0 - if int(ver_split[0]) > int(profile_ver_split[0]): + if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[0]) < int(profile_ver_split[0]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - else: - if int(ver_split[1]) > int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[1]) < int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[2]) > int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - - elif int(ver_split[2]) < int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionTooNew + 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()) @@ -178,8 +160,7 @@ class WaccaLily(WaccaS): def handle_user_status_getDetail_request(self, data: Dict)-> Dict: req = UserStatusGetDetailRequest(data) - ver_split = req.appVersion.split(".") - if int(ver_split[1]) >= 53: + if req.appVersion.minor >= 53: resp = UserStatusGetDetailResponseV3() else: resp = UserStatusGetDetailResponseV2() @@ -252,7 +233,7 @@ class WaccaLily(WaccaS): for user_gate in profile_gates: if user_gate["gate_id"] == gate: - if int(ver_split[1]) >= 53: + 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"])) @@ -266,7 +247,7 @@ class WaccaLily(WaccaS): break if not added_gate: - if int(ver_split[1]) >= 53: + if req.appVersion.minor >= 53: resp.gateInfo.append(GateDetailV2(gate)) else: From 382e36e60f76b69249876d493c6d18f54e347fda Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 21:49:00 -0500 Subject: [PATCH 050/589] core: Add county codes and Japanese region IDs --- core/const.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/core/const.py b/core/const.py index f5979e8..21c2da2 100644 --- a/core/const.py +++ b/core/const.py @@ -31,6 +31,64 @@ class KeychipPlatformsCodes(): NU = ("A60E", "A60E", "A60E") NUSX = ("A61X", "A69X") ALLS = "A63E" - + +class AllnetCountryCode(Enum): + JAPAN = "JPN" + UNITED_STATES = "USA" + HONG_KONG = "HKG" + SINGAPORE = "SGP" + SOUTH_KOREA = "KOR" + CHINA = "CHN" + +class AllnetJapanRegionId(Enum): + NONE = 0 + AICHI = 1 + AOMORI = 2 + AKITA = 3 + ISHIKAWA = 4 + IBARAKI = 5 + IWATE = 6 + EHIME = 7 + OITA = 8 + OSAKA = 9 + OKAYAMA = 10 + OKINAWA = 11 + KAGAWA = 12 + KAGOSHIMA = 13 + KANAGAWA = 14 + GIFU = 15 + KYOTO = 16 + KUMAMOTO = 17 + GUNMA = 18 + KOCHI = 19 + SAITAMA = 20 + SAGA = 21 + SHIGA = 22 + SHIZUOKA = 23 + SHIMANE = 24 + CHIBA = 25 + TOKYO = 26 + TOKUSHIMA = 27 + TOCHIGI = 28 + TOTTORI = 29 + TOYAMA = 30 + NAGASAKI = 31 + NAGANO = 32 + NARA = 33 + NIIGATA = 34 + HYOGO = 35 + HIROSHIMA = 36 + FUKUI = 37 + FUKUOKA = 38 + FUKUSHIMA = 39 + HOKKAIDO = 40 + MIE = 41 + MIYAGI = 42 + MIYAZAKI = 43 + YAMAGATA = 44 + YAMAGUCHI = 45 + YAMANASHI = 46 + WAKAYAMA = 47 + class RegionIDs(Enum): pass \ No newline at end of file From 078059f54e1ec9972c384429aac8a997a8ba0072 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 21:51:52 -0500 Subject: [PATCH 051/589] core: remove unused class from const --- core/const.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/const.py b/core/const.py index 21c2da2..3a2c116 100644 --- a/core/const.py +++ b/core/const.py @@ -89,6 +89,3 @@ class AllnetJapanRegionId(Enum): YAMAGUCHI = 45 YAMANASHI = 46 WAKAYAMA = 47 - -class RegionIDs(Enum): - pass \ No newline at end of file From 379388c74912de15f205fe81641a2b6673f730e9 Mon Sep 17 00:00:00 2001 From: Raymonf Date: Wed, 1 Mar 2023 22:01:35 -0500 Subject: [PATCH 052/589] wacca: allow setting prefecture in config by name --- example_config/wacca.yaml | 1 + titles/wacca/base.py | 102 ++++++++++++++++++++------------------ titles/wacca/config.py | 4 ++ titles/wacca/const.py | 1 + titles/wacca/lily.py | 2 +- 5 files changed, 61 insertions(+), 49 deletions(-) diff --git a/example_config/wacca.yaml b/example_config/wacca.yaml index 4c898a4..aea4f16 100644 --- a/example_config/wacca.yaml +++ b/example_config/wacca.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + prefecture_name: "Hokkaido" mods: always_vip: True diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 535048f..7d6424e 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -1,9 +1,8 @@ from typing import Any, List, Dict import logging +import inflection from math import floor - from datetime import datetime, timedelta - from core.config import CoreConfig from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants @@ -23,53 +22,60 @@ class WaccaBase(): self.season = 1 self.OPTIONS_DEFAULTS: Dict[str, Any] = { - "note_speed": 5, - "field_mask": 0, - "note_sound": 105001, - "note_color": 203001, - "bgm_volume": 10, - "bg_video": 0, - - "mirror": 0, - "judge_display_pos": 0, - "judge_detail_display": 0, - "measure_guidelines": 1, - "guideline_mask": 1, - "judge_line_timing_adjust": 10, - "note_design": 3, - "bonus_effect": 1, - "chara_voice": 1, - "score_display_method": 0, - "give_up": 0, - "guideline_spacing": 1, - "center_display": 1, - "ranking_display": 1, - "stage_up_icon_display": 1, - "rating_display": 1, - "player_level_display": 1, - "touch_effect": 1, - "guide_sound_vol": 3, - "touch_note_vol": 8, - "hold_note_vol": 8, - "slide_note_vol": 8, - "snap_note_vol": 8, - "chain_note_vol": 8, - "bonus_note_vol": 8, - "gate_skip": 0, - "key_beam_display": 1, + "note_speed": 5, + "field_mask": 0, + "note_sound": 105001, + "note_color": 203001, + "bgm_volume": 10, + "bg_video": 0, - "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 - } + "mirror": 0, + "judge_display_pos": 0, + "judge_detail_display": 0, + "measure_guidelines": 1, + "guideline_mask": 1, + "judge_line_timing_adjust": 10, + "note_design": 3, + "bonus_effect": 1, + "chara_voice": 1, + "score_display_method": 0, + "give_up": 0, + "guideline_spacing": 1, + "center_display": 1, + "ranking_display": 1, + "stage_up_icon_display": 1, + "rating_display": 1, + "player_level_display": 1, + "touch_effect": 1, + "guide_sound_vol": 3, + "touch_note_vol": 8, + "hold_note_vol": 8, + "slide_note_vol": 8, + "snap_note_vol": 8, + "chain_note_vol": 8, + "bonus_note_vol": 8, + "gate_skip": 0, + "key_beam_display": 1, + + "left_slide_note_color": 4, + "right_slide_note_color": 3, + "forward_slide_note_color": 1, + "back_slide_note_color": 2, + + "master_vol": 3, + "set_title_id": 104001, + "set_icon_id": 102001, + "set_nav_id": 210001, + "set_plate_id": 211001 + } self.allowed_stages = [] + + prefecture_name = inflection.underscore(game_cfg.server.prefecture_name).replace(' ', '_').upper() + if prefecture_name not in [region.name for region in WaccaConstants.Region]: + self.logger.warning(f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file") + self.region_id = 1 + else: + self.region_id = int(WaccaConstants.Region[prefecture_name].value) def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) @@ -86,7 +92,7 @@ class WaccaBase(): req = HousingStartRequestV1(data) resp = HousingStartResponseV1( - 1, + self.region_id, [ # Recomended songs 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, diff --git a/titles/wacca/config.py b/titles/wacca/config.py index f5bc235..fc03dd8 100644 --- a/titles/wacca/config.py +++ b/titles/wacca/config.py @@ -13,6 +13,10 @@ class WaccaServerConfig(): def loglevel(self) -> int: 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") + class WaccaModsConfig(): def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 0590f5f..0cca096 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -151,6 +151,7 @@ class WaccaConstants(): KAGOSHIMA = 46 OKINAWA = 47 UNITED_STATES = 48 + USA = 48 TAIWAN = 49 HONG_KONG = 50 SINGAPORE = 51 diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 77d37de..ac924b7 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -44,7 +44,7 @@ class WaccaLily(WaccaS): req = HousingStartRequestV2(data) resp = HousingStartResponseV1( - 1, + self.region_id, [ # Recomended songs 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, From e961c1dfb3f24d96bc90fe15c9179e4e79ac67ad Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 22:27:33 -0500 Subject: [PATCH 053/589] wacca: add region logic --- titles/wacca/base.py | 21 ++++------- titles/wacca/const.py | 4 ++ titles/wacca/handlers/helpers.py | 63 ++++++++++++++++---------------- titles/wacca/handlers/housing.py | 17 +++++++-- titles/wacca/lily.py | 17 +++------ 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 7d6424e..984cdad 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -73,9 +73,9 @@ class WaccaBase(): prefecture_name = inflection.underscore(game_cfg.server.prefecture_name).replace(' ', '_').upper() if prefecture_name not in [region.name for region in WaccaConstants.Region]: self.logger.warning(f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file") - self.region_id = 1 + self.region_id = WaccaConstants.Region.HOKKAIDO else: - self.region_id = int(WaccaConstants.Region[prefecture_name].value) + self.region_id = WaccaConstants.Region[prefecture_name] def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) @@ -91,17 +91,12 @@ class WaccaBase(): def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) - resp = HousingStartResponseV1( - self.region_id, - [ # Recomended songs - 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, - 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, - 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, - 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, - 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, - 1023,1015 - ] - ) + 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_advertise_GetNews_request(self, data: Dict) -> Dict: diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 0cca096..a984682 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -153,9 +153,13 @@ class WaccaConstants(): UNITED_STATES = 48 USA = 48 TAIWAN = 49 + TWN = 49 HONG_KONG = 50 + HKG = 50 SINGAPORE = 51 + SGP = 51 KOREA = 52 + KOR = 52 VALID_COUNTRIES = set(["JPN", "USA", "KOR", "HKG", "SGP"]) diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 752a656..9f6ba8b 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -90,8 +90,7 @@ class Version(ShortVersion): def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}.{self.country}.{self.role}.{self.build}" - -class HousingInfo(): +class HousingInfo: """ 1 is lan install role, 2 is country """ @@ -105,7 +104,7 @@ class HousingInfo(): def make(self) -> List: return [ self.id, self.val ] -class Notice(): +class Notice: name: str = "" title: str = "" message: str = "" @@ -128,7 +127,7 @@ class Notice(): 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(): +class UserOption: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: self.opt_id = opt_id self.opt_val = opt_val @@ -136,7 +135,7 @@ class UserOption(): def make(self) -> List: return [self.opt_id, self.opt_val] -class UserStatusV1(): +class UserStatusV1: def __init__(self) -> None: self.userId: int = 0 self.username: str = "" @@ -194,7 +193,7 @@ class PlayVersionStatus(Enum): VersionTooNew = 1 VersionUpgrade = 2 -class PlayModeCounts(): +class PlayModeCounts: seasonId: int = 0 modeId: int = 0 playNum: int = 0 @@ -211,7 +210,7 @@ class PlayModeCounts(): self.playNum ] -class SongUnlock(): +class SongUnlock: songId: int = 0 difficulty: int = 0 whenAppeared: int = 0 @@ -231,7 +230,7 @@ class SongUnlock(): self.whenUnlocked ] -class GenericItemRecv(): +class GenericItemRecv: def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None: self.itemId = item_id self.itemType = item_type @@ -240,7 +239,7 @@ class GenericItemRecv(): def make(self) -> List: return [ self.itemType, self.itemId, self.quantity ] -class GenericItemSend(): +class GenericItemSend: def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None: self.itemId = itemId self.itemType = itemType @@ -268,7 +267,7 @@ class IconItem(GenericItemSend): self.whenAcquired ] -class TrophyItem(): +class TrophyItem: trophyId: int = 0 season: int = 1 progress: int = 0 @@ -288,7 +287,7 @@ class TrophyItem(): self.badgeType ] -class TicketItem(): +class TicketItem: userTicketId: int = 0 ticketId: int = 0 whenExpires: int = 0 @@ -321,7 +320,7 @@ class NavigatorItem(IconItem): self.usesToday ] -class SkillItem(): +class SkillItem: skill_type: int level: int flag: int @@ -335,7 +334,7 @@ class SkillItem(): self.badge ] -class UserItemInfoV1(): +class UserItemInfoV1: def __init__(self) -> None: self.songUnlocks: List[SongUnlock] = [] self.titles: List[GenericItemSend] = [] @@ -419,7 +418,7 @@ class UserItemInfoV3(UserItemInfoV2): ret.append(effect) return ret -class SongDetailClearCounts(): +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: @@ -439,7 +438,7 @@ class SongDetailClearCounts(): def make(self) -> List: return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt] -class SongDetailGradeCountsV1(): +class SongDetailGradeCountsV1: dCt: int cCt: int bCt: int @@ -501,7 +500,7 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): def make(self) -> List: return super().make() + [self.spCt, self.sspCt, self.ssspCt] -class BestScoreDetailV1(): +class BestScoreDetailV1: songId: int = 0 difficulty: int = 1 clearCounts: SongDetailClearCounts = SongDetailClearCounts() @@ -534,7 +533,7 @@ class BestScoreDetailV1(): class BestScoreDetailV2(BestScoreDetailV1): gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2() -class SongUpdateJudgementCounts(): +class SongUpdateJudgementCounts: marvCt: int greatCt: int goodCt: int @@ -549,7 +548,7 @@ class SongUpdateJudgementCounts(): def make(self) -> List: return [self.marvCt, self.greatCt, self.goodCt, self.missCt] -class SongUpdateDetailV1(): +class SongUpdateDetailV1: def __init__(self, data: List) -> None: if data is not None: self.songId = data[0] @@ -579,7 +578,7 @@ class SongUpdateDetailV2(SongUpdateDetailV1): self.slowCt = data[14] self.flagNewRecord = False if data[15] == 0 else True -class SeasonalInfoV1(): +class SeasonalInfoV1: def __init__(self) -> None: self.level: int = 0 self.wpObtained: int = 0 @@ -613,7 +612,7 @@ class SeasonalInfoV2(SeasonalInfoV1): def make(self) -> List: return super().make() + [self.platesObtained, self.cumulativeGatePts] -class BingoPageStatus(): +class BingoPageStatus: id = 0 location = 1 progress = 0 @@ -626,7 +625,7 @@ class BingoPageStatus(): def make(self) -> List: return [self.id, self.location, self.progress] -class BingoDetail(): +class BingoDetail: def __init__(self, pageNumber: int) -> None: self.pageNumber = pageNumber self.pageStatus: List[BingoPageStatus] = [] @@ -641,7 +640,7 @@ class BingoDetail(): status ] -class GateDetailV1(): +class GateDetailV1: def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None: self.id = gate_id self.page = page @@ -657,11 +656,11 @@ class GateDetailV2(GateDetailV1): def make(self) -> List: return super().make() + [self.missionFlg] -class GachaInfo(): +class GachaInfo: def make(self) -> List: return [] -class LastSongDetail(): +class LastSongDetail: lastSongId = 90 lastSongDiff = 1 lastFolderOrd = 1 @@ -680,11 +679,11 @@ class LastSongDetail(): return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId, self.lastSongOrd] -class FriendDetail(): +class FriendDetail: def make(self) -> List: return [] -class LoginBonusInfo(): +class LoginBonusInfo: def __init__(self) -> None: self.tickets: List[TicketItem] = [] self.items: List[GenericItemRecv] = [] @@ -702,7 +701,7 @@ class LoginBonusInfo(): return [ tks, itms, self.message ] -class VipLoginBonus(): +class VipLoginBonus: id = 1 unknown = 0 item: GenericItemRecv @@ -715,7 +714,7 @@ class VipLoginBonus(): def make(self) -> List: return [ self.id, self.unknown, self.item.make() ] -class VipInfo(): +class VipInfo: def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None: self.pageYear = year self.pageMonth = month @@ -746,7 +745,7 @@ class PlayType(Enum): PlayTypeCoop = 3 PlayTypeStageup = 4 -class StageInfo(): +class StageInfo: danId: int = 0 danLevel: int = 0 clearStatus: int = 0 @@ -780,7 +779,7 @@ class StageupClearType(Enum): CLEAR_SILVER = 2 CLEAR_GOLD = 3 -class MusicUpdateDetailV1(): +class MusicUpdateDetailV1: def __init__(self) -> None: self.songId = 0 self.difficulty = 1 @@ -818,7 +817,7 @@ class MusicUpdateDetailV3(MusicUpdateDetailV2): super().__init__() self.grades = SongDetailGradeCountsV2() -class SongRatingUpdate(): +class SongRatingUpdate: def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None: self.songId = song_id self.difficulty = difficulty @@ -831,7 +830,7 @@ class SongRatingUpdate(): self.rating, ] -class GateTutorialFlag(): +class GateTutorialFlag: def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None: self.tutorialId = tutorial_id self.flagWatched = flg_watched diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index f49f1c2..1669cac 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -2,6 +2,7 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import HousingInfo +from titles.wacca.const import WaccaConstants # ---housing/get---- class HousingGetResponse(BaseResponse): @@ -37,12 +38,22 @@ class HousingStartRequestV2(HousingStartRequestV1): self.info.append(HousingInfo(info[0], info[1])) class HousingStartResponseV1(BaseResponse): - def __init__(self, regionId: int, songList: List[int]) -> None: + def __init__(self, regionId: WaccaConstants.Region = WaccaConstants.Region.HOKKAIDO, songList: List[int] = []) -> None: super().__init__() self.regionId = regionId - self.songList = songList + self.songList = songList # Recomended songs + + if not self.songList: + self.songList = [ + 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, + 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, + 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, + 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, + 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, + 1023,1015 + ] def make(self) -> Dict: - self.params = [self.regionId, self.songList] + self.params = [self.regionId.value, self.songList] return super().make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index ac924b7..d15a504 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -42,18 +42,13 @@ class WaccaLily(WaccaS): 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]: + region_id = WaccaConstants.Region[req.appVersion.country] + else: + region_id = self.region_id - resp = HousingStartResponseV1( - self.region_id, - [ # Recomended songs - 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, - 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, - 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, - 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, - 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, - 1023,1015 - ] - ) + resp = HousingStartResponseV1(region_id) return resp.make() def handle_user_status_get_request(self, data: Dict)-> Dict: From a0739436cc228337dfec78eee581126b1b0abfb8 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 23:03:29 -0500 Subject: [PATCH 054/589] Wacca: Fix stageup order, fixes #3 --- titles/wacca/base.py | 13 ++++++++----- titles/wacca/lily.py | 26 +++++++++++++------------- titles/wacca/lilyr.py | 26 +++++++++++++------------- titles/wacca/reverse.py | 26 +++++++++++++------------- titles/wacca/s.py | 24 ++++++++++++------------ 5 files changed, 59 insertions(+), 56 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 984cdad..2bf47fd 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -361,7 +361,8 @@ class WaccaBase(): if stages is None: stages = [] - add_next = True + tmp: List[StageInfo] = [] + for d in self.allowed_stages: stage_info = StageInfo(d[0], d[1]) @@ -374,11 +375,13 @@ class WaccaBase(): stage_info.song3BestScore = score["song3_score"] break - if add_next or stage_info.danLevel < 9: - resp.stageList.append(stage_info) + tmp.append(stage_info) - if stage_info.danLevel >= 9 and stage_info.clearStatus < 1: - add_next = False + for x in range(len(resp.stageList)): + if resp.stageList[x].danLevel >= 10 and (resp.stageList[x + 1].clearStatus >= 1 or resp.stageList[x].clearStatus >= 1): + resp.stageList.append(tmp[x]) + elif resp.stageList[x].danLevel < 10: + resp.stageList.append(tmp[x]) return resp.make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index d15a504..66a4bc8 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -17,20 +17,20 @@ class WaccaLily(WaccaS): self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.allowed_stages = [ - (2001, 1), - (2002, 2), - (2003, 3), - (2004, 4), - (2005, 5), - (2006, 6), - (2007, 7), - (2008, 8), - (2009, 9), - (2010, 10), - (2011, 11), - (2012, 12), - (2013, 13), (2014, 14), + (2013, 13), + (2012, 12), + (2011, 11), + (2010, 10), + (2009, 9), + (2008, 8), + (2007, 7), + (2006, 6), + (2005, 5), + (2004, 4), + (2003, 3), + (2002, 2), + (2001, 1), (210001, 0), (210002, 0), (210003, 0), diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index 559c61d..893a8f3 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -16,20 +16,20 @@ class WaccaLilyR(WaccaLily): self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.allowed_stages = [ - (2501, 1), - (2502, 2), - (2503, 3), - (2504, 4), - (2505, 5), - (2506, 6), - (2507, 7), - (2508, 8), - (2509, 9), - (2510, 10), - (2511, 11), - (2512, 12), - (2513, 13), (2514, 14), + (2513, 13), + (2512, 12), + (2511, 11), + (2510, 10), + (2509, 9), + (2508, 8), + (2507, 7), + (2506, 6), + (2505, 5), + (2504, 4), + (2503, 3), + (2501, 2), + (2501, 1), (210001, 0), (210002, 0), (210003, 0), diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 78db7e4..100ffb1 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -18,20 +18,20 @@ class WaccaReverse(WaccaLilyR): self.OPTIONS_DEFAULTS["set_nav_id"] = 310001 self.allowed_stages = [ - (3001, 1), - (3002, 2), - (3003, 3), - (3004, 4), - (3005, 5), - (3006, 6), - (3007, 7), - (3008, 8), - (3009, 9), - (3010, 10), - (3011, 11), - (3012, 12), - (3013, 13), (3014, 14), + (3013, 13), + (3012, 12), + (3011, 11), + (3010, 10), + (3009, 9), + (3008, 8), + (3007, 7), + (3006, 6), + (3005, 5), + (3004, 4), + (3003, 3), + (3002, 2), + (3001, 1), # Touhou (210001, 0), (210002, 0), diff --git a/titles/wacca/s.py b/titles/wacca/s.py index 9f23367..d4c2881 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -11,19 +11,19 @@ from titles.wacca.handlers import * class WaccaS(WaccaBase): allowed_stages = [ - (1501, 1), - (1502, 2), - (1503, 3), - (1504, 4), - (1505, 5), - (1506, 6), - (1507, 7), - (1508, 8), - (1509, 9), - (1510, 10), - (1511, 11), - (1512, 12), (1513, 13), + (1512, 12), + (1511, 11), + (1510, 10), + (1509, 9), + (1508, 8), + (1507, 7), + (1506, 6), + (1505, 5), + (1514, 4), + (1513, 3), + (1512, 2), + (1511, 1), ] def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: From 1567ec23ab791e4481e11ac51c26fac965cea538 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 23:24:36 -0500 Subject: [PATCH 055/589] wacca: fix stageup list not populating correctly, fix #3 --- titles/wacca/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 2bf47fd..9df231e 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -356,7 +356,6 @@ class WaccaBase(): 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 = [] @@ -377,10 +376,10 @@ class WaccaBase(): tmp.append(stage_info) - for x in range(len(resp.stageList)): - if resp.stageList[x].danLevel >= 10 and (resp.stageList[x + 1].clearStatus >= 1 or resp.stageList[x].clearStatus >= 1): + for x in range(len(tmp)): + if tmp[x].danLevel >= 10 and (tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1): resp.stageList.append(tmp[x]) - elif resp.stageList[x].danLevel < 10: + elif tmp[x].danLevel < 10: resp.stageList.append(tmp[x]) return resp.make() From be00ad8e961eafc79f5efc1bfa805b791502da93 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 23:55:29 -0500 Subject: [PATCH 056/589] wacca: add lily to list of items given on profile create, fixes #4 --- titles/wacca/lily.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 66a4bc8..e67b25c 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -50,6 +50,14 @@ class WaccaLily(WaccaS): resp = HousingStartResponseV1(region_id) return resp.make() + + def handle_user_status_create_request(self, data: Dict)-> Dict: + req = UserStatusCreateRequest(data) + resp = super().handle_user_status_create_request(data) + + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002) # Lily, Added Lily + + return resp def handle_user_status_get_request(self, data: Dict)-> Dict: req = UserStatusGetRequest(data) From 6b0838062eff5e73402b1df3dcb4ec0569ccd077 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 1 Mar 2023 23:55:29 -0500 Subject: [PATCH 057/589] wacca: add lily to list of items given on profile create, fixes #4 --- titles/wacca/lily.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 66a4bc8..e67b25c 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -50,6 +50,14 @@ class WaccaLily(WaccaS): resp = HousingStartResponseV1(region_id) return resp.make() + + def handle_user_status_create_request(self, data: Dict)-> Dict: + req = UserStatusCreateRequest(data) + resp = super().handle_user_status_create_request(data) + + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002) # Lily, Added Lily + + return resp def handle_user_status_get_request(self, data: Dict)-> Dict: req = UserStatusGetRequest(data) From 5965362a0f612600840366cbf010643d6bb83d8c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 00:14:13 -0500 Subject: [PATCH 058/589] title: add 405 and 404 error responses --- core/title.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/title.py b/core/title.py index 6c54450..252a130 100644 --- a/core/title.py +++ b/core/title.py @@ -53,10 +53,13 @@ class TitleServlet(): 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_GET"): self.logger.warn(f"{code} does not dispatch GET") + request.setResponseCode(405) return b"" return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) @@ -65,10 +68,13 @@ class TitleServlet(): 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"]) From 7071ab0bd907f1c06c9b1dd771580913882220a2 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 00:24:04 -0500 Subject: [PATCH 059/589] chuni: add IP logging, clean up logs --- titles/chuni/index.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 25f31b5..a0f8b55 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -135,7 +135,8 @@ class ChuniServlet(): req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + 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" @@ -154,7 +155,7 @@ class ChuniServlet(): if resp == None: resp = {'returnCode': 1} - self.logger.info(f"Response {resp}") + self.logger.debug(f"Response {resp}") zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) From 846a556c5bcafa2c879453f46327a31086601370 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 10:28:49 -0500 Subject: [PATCH 060/589] fix nginx example --- example_config/nginx_example.conf | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index 096a072..30c733c 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -99,26 +99,6 @@ server { } } -# CXB, comment this out if you don't plan on serving crossbeats. -server { - listen 443 ssl; - server_name cxb.hostname.here; - - ssl_certificate /path/to/cert/cxb.pem; - ssl_certificate_key /path/to/cert/cxb.key; - ssl_session_timeout 1d; - ssl_session_cache shared:MozSSL:10m; - ssl_session_tickets off; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; - ssl_ciphers "ALL:@SECLEVEL=1"; - ssl_prefer_server_ciphers off; - - location / { - proxy_pass http://localhost:8080/SDBT/104/; - } -} - # Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend server { listen 80; From d5a7247a7f9d0fa87f51b1d53a0596bde9e4bc75 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 11:47:40 -0500 Subject: [PATCH 061/589] add production config --- docs/prod.md | 35 +++++++++++++++++++++++++++++++ example_config/nginx_example.conf | 1 + 2 files changed, 36 insertions(+) create mode 100644 docs/prod.md diff --git a/docs/prod.md b/docs/prod.md new file mode 100644 index 0000000..2967679 --- /dev/null +++ b/docs/prod.md @@ -0,0 +1,35 @@ +# ARTEMiS Production mode +Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode". + +## Nginx Configuration +### Port forwarding +Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games. +`tcp:80` all.net, non-ssl titles +`tcp:8443` billing +`tcp:22345` aimedb +`tcp:443` frontend, SSL titles + +### A note about external proxy services (cloudflare, etc) +Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc) + +### Port mappings +An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following: +`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet +`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server +`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server +`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server +`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request) +`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken +`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection + +If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificat paths correctly, as in the example they are simply placeholders. + +### Multi-service ports +It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same) +`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server +`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server +`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server +`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request) +`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken + +This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before. \ No newline at end of file diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index 30c733c..fe6f7a7 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -114,6 +114,7 @@ server { # Frontend HTTPS. Comment out if you on't intend to use the frontend server { listen 443 ssl; + server_name frontend.hostname.here; ssl_certificate /path/to/cert/frontend.pem; ssl_certificate_key /path/to/cert/frontend.key; From 99881ea2208defc8303c9ef090ef9d743901646e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 11:54:50 -0500 Subject: [PATCH 062/589] docs: Add note about SSL certs to prod.md --- docs/prod.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/prod.md b/docs/prod.md index 2967679..35785f2 100644 --- a/docs/prod.md +++ b/docs/prod.md @@ -12,6 +12,12 @@ Artemis requires that the following ports be forwarded to allow internet traffic ### A note about external proxy services (cloudflare, etc) Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc) +### SSL Certificates +You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below +- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here) +- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB +- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected. + ### Port mappings An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following: `naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet From 4d9ae19cb295c2e7f38194496d1a6bab503caf86 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 11:59:52 -0500 Subject: [PATCH 063/589] update readme --- readme.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 50bf933..1e05d5a 100644 --- a/readme.md +++ b/readme.md @@ -29,10 +29,8 @@ Games listed below have been tested and confirmed working. Only game versions ol - memcached (for non-windows platforms) - mysql/mariadb server -## Quick start guide -1) Clone this repository -2) Install requirements (see the platform-specific guides for instructions) -3) Install python libraries via `pip` -4) Copy the example configuration files into another folder (by default the server looks for the `config` directory) -5) Edit the newly copied configuration files to your liking, using [this](docs/config.md) doc as a guide. -6) Run the server by invoking `index.py` ex. `python3 index.py` \ No newline at end of file +## 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. + +## Production guide +See the [production guide](docs/prod.md) for running a production server. From e205777693e2e229ea7c7dcbb0fd5b819b3d47d0 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 12:05:38 -0500 Subject: [PATCH 064/589] docs: fix typo in prod.md --- docs/prod.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prod.md b/docs/prod.md index 35785f2..de79b99 100644 --- a/docs/prod.md +++ b/docs/prod.md @@ -28,7 +28,7 @@ An example config is provided in the `config` folder called `nginx_example.conf` `pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken `frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection -If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificat paths correctly, as in the example they are simply placeholders. +If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders. ### Multi-service ports It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same) From c5fc879af669251462d268eb4992a0044ec53b89 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 2 Mar 2023 13:02:43 -0500 Subject: [PATCH 065/589] core: add taiwan to AllnetCountryCode --- core/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/const.py b/core/const.py index 3a2c116..da018ee 100644 --- a/core/const.py +++ b/core/const.py @@ -38,6 +38,7 @@ class AllnetCountryCode(Enum): HONG_KONG = "HKG" SINGAPORE = "SGP" SOUTH_KOREA = "KOR" + TAIWAN = "TWN" CHINA = "CHN" class AllnetJapanRegionId(Enum): From 937dba20cafdc4ab47d1b4aff686e84d223a8aec Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 2 Mar 2023 22:13:18 -0500 Subject: [PATCH 066/589] Fixing billing that was failing for Chunithm --- core/allnet.py | 2 +- index.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index c938a88..587aeab 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -191,7 +191,7 @@ class AllnetServlet: self.logger.debug(f"request {req_dict}") - rsa = RSA.import_key(open(self.config.billing.sign_key, 'rb').read()) + rsa = RSA.import_key(open(self.config.billing.signing_key, 'rb').read()) signer = PKCS1_v1_5.new(rsa) digest = SHA.new() diff --git a/index.py b/index.py index e514b10..51842bd 100644 --- a/index.py +++ b/index.py @@ -29,6 +29,7 @@ class HttpDispatcher(resource.Resource): 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'])) From 3791b2b2383c9a13aa896b6cc821522d9f0a0ee4 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 3 Mar 2023 00:00:22 -0500 Subject: [PATCH 067/589] Adding Ongeki Bright Memory support --- core/data/schema/versions/SDDT_2_rollback.sql | 7 +++ core/data/schema/versions/SDDT_3_upgrade.sql | 27 +++++++++ readme.md | 2 +- titles/ongeki/base.py | 4 ++ titles/ongeki/brightmemory.py | 58 +++++++++++++++++++ titles/ongeki/const.py | 3 +- titles/ongeki/index.py | 6 +- titles/ongeki/schema/item.py | 40 +++++++++++++ titles/ongeki/schema/profile.py | 1 + titles/ongeki/schema/score.py | 3 + 10 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 core/data/schema/versions/SDDT_2_rollback.sql create mode 100644 core/data/schema/versions/SDDT_3_upgrade.sql create mode 100644 titles/ongeki/brightmemory.py diff --git a/core/data/schema/versions/SDDT_2_rollback.sql b/core/data/schema/versions/SDDT_2_rollback.sql new file mode 100644 index 0000000..5ddf1b6 --- /dev/null +++ b/core/data/schema/versions/SDDT_2_rollback.sql @@ -0,0 +1,7 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE ongeki_profile_data DROP COLUMN isDialogWatchedSuggestMemory; +ALTER TABLE ongeki_score_best DROP COLUMN platinumScoreMax; +ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScore; +ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScoreMax; +DROP TABLE IF EXISTS `ongeki_user_memorychapter`; +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_3_upgrade.sql b/core/data/schema/versions/SDDT_3_upgrade.sql new file mode 100644 index 0000000..26c5c30 --- /dev/null +++ b/core/data/schema/versions/SDDT_3_upgrade.sql @@ -0,0 +1,27 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_profile_data ADD COLUMN isDialogWatchedSuggestMemory BOOLEAN; +ALTER TABLE ongeki_score_best ADD COLUMN platinumScoreMax INTEGER; +ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScore INTEGER; +ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScoreMax INTEGER; + +CREATE TABLE ongeki_user_memorychapter ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + chapterId INT NOT NULL, + gaugeId INT NOT NULL, + gaugeNum INT NOT NULL, + jewelCount INT NOT NULL, + isStoryWatched BOOLEAN NOT NULL, + isBossWatched BOOLEAN NOT NULL, + isDialogWatched BOOLEAN NOT NULL, + isEndingWatched BOOLEAN NOT NULL, + isClear BOOLEAN NOT NULL, + lastPlayMusicId INT NOT NULL, + lastPlayMusicLevel INT NOT NULL, + lastPlayMusicCategory INT NOT NULL, + UNIQUE KEY ongeki_user_memorychapter_uk (user, chapterId), + CONSTRAINT ongeki_user_memorychapter_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/readme.md b/readme.md index 1e05d5a..24440fc 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + All versions + Ongeki - + All versions up to Bright + + All versions up to Bright Memory + Wacca + Lily R diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 8778645..3f6dc7a 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -787,6 +787,10 @@ class OngekiBase(): 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"]: diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py new file mode 100644 index 0000000..a99f806 --- /dev/null +++ b/titles/ongeki/brightmemory.py @@ -0,0 +1,58 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict +import pytz +import json + +from core.config import CoreConfig +from titles.ongeki.base import OngekiBase +from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig + +class OngekiBrightMemory(OngekiBase): + + def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["dataVersion"] = "1.35.00" + ret["gameSetting"]["onlineDataVersion"] = "1.35.00" + ret["gameSetting"]["maxCountCharacter"] = 50 + ret["gameSetting"]["maxCountCard"] = 300 + ret["gameSetting"]["maxCountItem"] = 300 + ret["gameSetting"]["maxCountMusic"] = 50 + ret["gameSetting"]["maxCountMusicItem"] = 300 + ret["gameSetting"]["maxCountRivalMusic"] = 300 + return ret + + def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: + memories = self.data.item.get_memorychapters(data["userId"]) + if not memories: + return {"userId": data["userId"], "length":6, "userMemoryChapterList":[ + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70003, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70004, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, + {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0} + ]} + + memory_chp = [] + for chp in memories: + tmp = chp._asdict() + tmp.pop("id") + tmp.pop("user") + memory_chp.append(tmp) + + return { + "userId": data["userId"], + "length": len(memory_chp), + "userMemoryChapterList": memory_chp + } + + def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: + return { + "techScore": 0, + "cardNum": 0 + } \ No newline at end of file diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index da9614e..dd6c0f5 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -10,6 +10,7 @@ class OngekiConstants(): VER_ONGEKI_RED = 4 VER_ONGEKI_RED_PLUS = 5 VER_ONGEKI_BRIGHT = 6 + VER_ONGEKI_BRIGHT_MEMORY = 7 EVT_TYPES: Enum = Enum('EVT_TYPES', [ 'None', @@ -43,7 +44,7 @@ class OngekiConstants(): Lunatic = 10 VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+", - "ONGEKI Bright") + "ONGEKI Bright", "ONGEKI Bright Memory") @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index d3b107e..c636659 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -17,6 +17,7 @@ from titles.ongeki.summerplus import OngekiSummerPlus from titles.ongeki.red import OngekiRed from titles.ongeki.redplus import OngekiRedPlus from titles.ongeki.bright import OngekiBright +from titles.ongeki.brightmemory import OngekiBrightMemory class OngekiServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -32,6 +33,7 @@ class OngekiServlet(): OngekiRed(core_cfg, self.game_cfg), OngekiRedPlus(core_cfg, self.game_cfg), OngekiBright(core_cfg, self.game_cfg), + OngekiBrightMemory(core_cfg, self.game_cfg), ] self.logger = logging.getLogger("ongeki") @@ -69,8 +71,10 @@ class OngekiServlet(): internal_ver = OngekiConstants.VER_ONGEKI_RED elif version >= 125 and version < 130: # Red Plus internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS - elif version >= 130 and version < 135: # Red Plus + elif version >= 130 and version < 135: # Bright internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT + elif version >= 135 and version < 140: # Bright Memory + internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index cc09fda..c3f8c7e 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -107,6 +107,27 @@ chapter = Table( mysql_charset='utf8mb4' ) +memorychapter = Table( + "ongeki_user_memorychapter", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("chapterId", Integer), + Column("gaugeId", Integer), + Column("gaugeNum", Integer), + Column("jewelCount", Integer), + Column("isStoryWatched", Boolean), + Column("isBossWatched", Boolean), + Column("isDialogWatched", Boolean), + Column("isEndingWatched", Boolean), + Column("isClear", Boolean), + Column("lastPlayMusicId", Integer), + Column("lastPlayMusicLevel", Integer), + Column("lastPlayMusicCategory", Integer), + UniqueConstraint("user", "chapterId", name="ongeki_user_memorychapter_uk"), + mysql_charset='utf8mb4' +) + item = Table( "ongeki_user_item", metadata, @@ -522,5 +543,24 @@ class OngekiItemData(BaseData): sql = select(boss).where(boss.c.user == aime_id) result = self.execute(sql) + if result is None: return None + return result.fetchall() + + def put_memorychapter(self, aime_id: int, memorychapter_data: Dict) -> Optional[int]: + memorychapter_data["user"] = aime_id + + sql = insert(memorychapter).values(**memorychapter_data) + conflict = sql.on_duplicate_key_update(**memorychapter_data) + result = self.execute(conflict) + + if result is None: + self.logger.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 diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index bdff67d..26c79b5 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -78,6 +78,7 @@ profile = Table( Column("overDamageBattlePoint", Integer, server_default="0"), Column("bestBattlePoint", Integer, server_default="0"), Column("lastEmoneyBrand", Integer, server_default="0"), + Column("isDialogWatchedSuggestMemory", Boolean), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), mysql_charset='utf8mb4' ) diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index 08a6a86..e526005 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -28,6 +28,7 @@ score_best = Table( Column("isLock", Boolean, nullable=False), Column("clearStatus", Boolean, nullable=False), Column("isStoryWatched", Boolean, nullable=False), + Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), mysql_charset='utf8mb4' ) @@ -96,6 +97,8 @@ playlog = Table( Column("isAllBreak", Boolean), Column("playerRating", Integer), Column("battlePoint", Integer), + Column("platinumScore", Integer), + Column("platinumScoreMax", Integer), mysql_charset='utf8mb4' ) From 4626ec36cd2426be2f1da71073a38bdc0416b544 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 12:40:03 -0500 Subject: [PATCH 068/589] wacca: fix options not saving --- titles/wacca/base.py | 2 +- titles/wacca/schema/profile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 9df231e..25ec33b 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -746,7 +746,7 @@ class WaccaBase(): user_id = self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: - self.data.profile.update_option(user_id, opt.id, opt.val) + self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val) for update in req.datesUpdated: pass diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index 7237149..c2a15f6 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -211,7 +211,7 @@ class WaccaProfileData(BaseData): ) conflict = sql.on_duplicate_key_update( - value = sql.inserted.value + value = value ) result = self.execute(conflict) From 7953519e683b46e026fa0d362ad71081d575df7e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 13:03:48 -0500 Subject: [PATCH 069/589] wacca: fix UserInfoUpdateRequest, per #5 --- titles/wacca/handlers/helpers.py | 8 ++++++++ titles/wacca/handlers/user_info.py | 10 +++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 9f6ba8b..1192237 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -840,3 +840,11 @@ class GateTutorialFlag: 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/user_info.py b/titles/wacca/handlers/user_info.py index 6498488..5665676 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -1,7 +1,7 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import UserOption +from titles.wacca.handlers.helpers import UserOption, DateUpdate # ---user/info/update--- class UserInfoUpdateRequest(BaseRequest): @@ -9,12 +9,16 @@ class UserInfoUpdateRequest(BaseRequest): super().__init__(data) self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] - self.datesUpdated: List = self.params[3] + self.unknown2: List = self.params[2] + self.datesUpdated: List[DateUpdate] = [] self.favoritesAdded: List[int] = self.params[4] self.favoritesRemoved: List[int] = self.params[5] - for x in self.params[2]: + for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) + + for x in self.params[3]: + self.datesUpdated.append(DateUpdate[x[0], x[1]]) # ---user/info/getMyroom--- TODO: Understand this better class UserInfogetMyroomRequest(BaseRequest): From cd78ecd7ea6cd4b7449c657a52cc528396d4ec72 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 13:07:10 -0500 Subject: [PATCH 070/589] wacca: fix typo in UserInfoUpdateRequest --- titles/wacca/handlers/user_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 5665676..287b742 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -18,7 +18,7 @@ class UserInfoUpdateRequest(BaseRequest): self.optsUpdated.append(UserOption(x[0], x[1])) for x in self.params[3]: - self.datesUpdated.append(DateUpdate[x[0], x[1]]) + self.datesUpdated.append(DateUpdate([x[0], x[1]])) # ---user/info/getMyroom--- TODO: Understand this better class UserInfogetMyroomRequest(BaseRequest): From 2da12e515e35ef2abd7581d761a96c66d8b67306 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 13:07:36 -0500 Subject: [PATCH 071/589] wacca: see previous --- titles/wacca/handlers/user_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 287b742..d30da39 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -18,7 +18,7 @@ class UserInfoUpdateRequest(BaseRequest): self.optsUpdated.append(UserOption(x[0], x[1])) for x in self.params[3]: - self.datesUpdated.append(DateUpdate([x[0], x[1]])) + self.datesUpdated.append(DateUpdate(x[0], x[1])) # ---user/info/getMyroom--- TODO: Understand this better class UserInfogetMyroomRequest(BaseRequest): From 45fedd8425d6fc8e29e0bcc4f671a374d381c3bb Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 13:12:06 -0500 Subject: [PATCH 072/589] wacca: tidy up UserStatusUpdateRequestV2 --- titles/wacca/handlers/user_status.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index b852dd1..5874fec 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -275,11 +275,6 @@ class UserStatusUpdateRequestV1(BaseRequest): self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2])) class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1): - isContinue = False - isFirstPlayFree = False - itemsUsed = [] - lastSongInfo: LastSongDetail - def __init__(self, data: Dict) -> None: super().__init__(data) self.isContinue = bool(data["params"][3]) From 4c64554383c5de1b9b87583f7a4896c1794df828 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 3 Mar 2023 13:27:22 -0500 Subject: [PATCH 073/589] pushing small typo for title port on both guides --- docs/INSTALL_UBUNTU.md | 2 +- docs/INSTALL_WINDOWS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/INSTALL_UBUNTU.md b/docs/INSTALL_UBUNTU.md index eaabb0c..710c757 100644 --- a/docs/INSTALL_UBUNTU.md +++ b/docs/INSTALL_UBUNTU.md @@ -96,7 +96,7 @@ sudo ufw allow 8443 sudo ufw allow 22345 sudo ufw allow 8090 sudo ufw allow 8444 -sudo ufw allow 9000 +sudo ufw allow 8080 ``` ## Running the ARTEMiS instance diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index abd1e43..e88def3 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -57,7 +57,7 @@ title: ## Firewall Adjustements Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): -> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha, 9000 (TCP) +> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha ## Running the ARTEMiS instance > python index.py From fae6b77403926ed0b7e83e60022579effc6a4597 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 15:03:57 -0500 Subject: [PATCH 074/589] core: TESTING fix for get_machine --- core/data/schema/arcade.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 117c3fe..921e5b5 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -50,9 +50,20 @@ arcade_owner = Table( class ArcadeData(BaseData): def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: if serial is not None: - sql = machine.select(machine.c.serial == serial) + serial = serial.replace("-", "") + if len(serial) == 11: + sql = machine.select(machine.c.serial.like(f"{serial}%")) + + elif len(serial) == 15: + sql = machine.select(machine.c.serial == serial) + + else: + self.logger.error(f"{__name__ }: Malformed serial {serial}") + return None + elif id is not None: sql = machine.select(machine.c.id == id) + else: self.logger.error(f"{__name__ }: Need either serial or ID to look up!") return None @@ -110,4 +121,4 @@ class ArcadeData(BaseData): return result.lastrowid def generate_keychip_serial(self, platform_id: int) -> str: - pass \ No newline at end of file + pass From 101b966e3a4f2f80f8acaab331b1297a22e51e02 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 15:39:14 -0500 Subject: [PATCH 075/589] add allnet request debug logging --- core/allnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 587aeab..01a3610 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -115,6 +115,7 @@ class AllnetServlet: else: resp = AllnetPowerOnResponse2() + 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) @@ -154,6 +155,7 @@ class AllnetServlet: 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) + self.logger.debug(f"Allnet response: {vars(resp)}") return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") From f6cfb9e36dbdf3f56896a978bf57326067291019 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 15:45:21 -0500 Subject: [PATCH 076/589] allnet: fix "none" in response --- core/allnet.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 01a3610..4f886f9 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -14,6 +14,7 @@ from time import strptime from core.config import CoreConfig 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): @@ -137,15 +138,15 @@ class AllnetServlet: if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - resp.country = arcade["country"] if machine["country"] is None else machine["country"] + resp.country = arcade["country"] if machine["country"] is None else machine["country"] if machine["country"] is None else AllnetCountryCode.JAPAN.value resp.place_id = arcade["id"] resp.allnet_id = machine["id"] - resp.name = arcade["name"] - resp.nickname = arcade["nickname"] - resp.region0 = arcade["region_id"] - resp.region_name0 = arcade["country"] - resp.region_name1 = arcade["state"] - resp.region_name2 = arcade["city"] + 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.region_name2 = arcade["city"] if arcade["city"] is not None else "" resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" int_ver = req.ver.replace(".", "") From b35e7d69831ee6ae13c3080a6bd16343e05ce4ed Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 15:49:33 -0500 Subject: [PATCH 077/589] allnet: hotfix for country --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 4f886f9..233469a 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -138,7 +138,7 @@ class AllnetServlet: if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - resp.country = arcade["country"] if machine["country"] is None else machine["country"] if machine["country"] is None else AllnetCountryCode.JAPAN.value + resp.country = arcade["country"] if machine["country"] is None else machine["country"] if machine["country"] is not None else AllnetCountryCode.JAPAN.value resp.place_id = arcade["id"] resp.allnet_id = machine["id"] resp.name = arcade["name"] if arcade["name"] is not None else "" From 34e2c50fb5b1fad897c6f69ba60f5687fe3f3029 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 15:52:58 -0500 Subject: [PATCH 078/589] allnet: see previous --- core/allnet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 233469a..71eeb33 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -138,7 +138,11 @@ class AllnetServlet: if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - resp.country = arcade["country"] if machine["country"] is None else machine["country"] if machine["country"] is not None else AllnetCountryCode.JAPAN.value + country = arcade["country"] if machine["country"] is None else machine["country"] + if country is None: + country = AllnetCountryCode.JAPAN.value + + resp.country = country resp.place_id = arcade["id"] resp.allnet_id = machine["id"] resp.name = arcade["name"] if arcade["name"] is not None else "" From f24d554a448673d67dfdc52286e5301a7832f278 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 16:26:07 -0500 Subject: [PATCH 079/589] wacca: pull region_id from allnet if available --- titles/wacca/base.py | 21 +++++++++++++++++++-- titles/wacca/const.py | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 25ec33b..64b4248 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -91,10 +91,27 @@ class WaccaBase(): def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) - if req.appVersion.country != "JPN" and req.appVersion.country in [region.name for region in WaccaConstants.Region]: + machine = self.data.arcade.get_machine(req.chipId) + if machine is not None: + arcade = self.data.arcade.get_arcade(machine["arcade"]) + + allnet_region_id = arcade["region_id"] + + if req.appVersion.country == "JPN": + if allnet_region_id is not None: + region = WaccaConstants.allnet_region_id_to_wacca_region(allnet_region_id) + + if region is None: + region_id = self.region_id + + else: + region_id = self.region_id + + elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES: region_id = WaccaConstants.Region[req.appVersion.country] + else: - region_id = self.region_id + region_id = 0 resp = HousingStartResponseV1(region_id) return resp.make() diff --git a/titles/wacca/const.py b/titles/wacca/const.py index a984682..f072143 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -1,4 +1,7 @@ from enum import Enum +from typing import Optional + +from core.const import AllnetJapanRegionId class WaccaConstants(): CONFIG_NAME = "wacca.yaml" @@ -165,4 +168,22 @@ class WaccaConstants(): @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] + + @classmethod + def allnet_region_id_to_wacca_region(cls, region: int) -> Optional[Region]: + try: + return [ + cls.Region.NONE, cls.Region.AICHI, cls.Region.AOMORI, cls.Region.AKITA, cls.Region.ISHIKAWA, + cls.Region.IBARAKI, cls.Region.IWATE, cls.Region.EHIME, cls.Region.OITA, cls.Region.OSAKA, + cls.Region.OKAYAMA, cls.Region.OKINAWA, cls.Region.KAGAWA, cls.Region.KAGOSHIMA, cls.Region.KANAGAWA, + cls.Region.GIFU, cls.Region.KYOTO, cls.Region.KUMAMOTO, cls.Region.GUNMA, cls.Region.KOCHI, + cls.Region.SAITAMA, cls.Region.SAGA, cls.Region.SHIGA, cls.Region.SHIZUOKA, cls.Region.SHIMANE, + cls.Region.CHIBA, cls.Region.TOKYO, cls.Region.TOKUSHIMA, cls.Region.TOCHIGI, cls.Region.TOTTORI, + cls.Region.TOYAMA, cls.Region.NAGASAKI, cls.Region.NAGANO, cls.Region.NARA, cls.Region.NIIGATA, + cls.Region.HYOGO, cls.Region.HIROSHIMA, cls.Region.FUKUI, cls.Region.FUKUOKA, cls.Region.FUKUSHIMA, + cls.Region.HOKKAIDO, cls.Region.MIE, cls.Region.MIYAGI, cls.Region.MIYAZAKI, cls.Region.YAMAGATA, + cls.Region.YAMAGUCHI, cls.Region.YAMANASHI, cls.Region.WAKAYAMA, + ][region] + except: return None + From 9ad724d64beee2f0c27891acfe1d8452b614dc7c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 16:28:42 -0500 Subject: [PATCH 080/589] wacca: fix edge case in handle_housing_start_request --- titles/wacca/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 64b4248..9867d5a 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -103,6 +103,8 @@ class WaccaBase(): if region is None: region_id = self.region_id + else: + region_id = region else: region_id = self.region_id @@ -111,7 +113,7 @@ class WaccaBase(): region_id = WaccaConstants.Region[req.appVersion.country] else: - region_id = 0 + region_id = WaccaConstants.Region.NONE resp = HousingStartResponseV1(region_id) return resp.make() From c26f6b7b1d500b052140079dcad99ca2630e3c70 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 16:31:42 -0500 Subject: [PATCH 081/589] wacca: fix typing --- titles/wacca/base.py | 5 +++-- titles/wacca/handlers/helpers.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 9867d5a..2e5001f 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.database import WaccaData from titles.wacca.handlers import * +from core.const import AllnetCountryCode class WaccaBase(): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: @@ -74,6 +75,7 @@ class WaccaBase(): if prefecture_name not in [region.name for region in WaccaConstants.Region]: self.logger.warning(f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file") self.region_id = WaccaConstants.Region.HOKKAIDO + else: self.region_id = WaccaConstants.Region[prefecture_name] @@ -94,10 +96,9 @@ class WaccaBase(): machine = self.data.arcade.get_machine(req.chipId) if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - allnet_region_id = arcade["region_id"] - if req.appVersion.country == "JPN": + 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) diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 1192237..b96b3dd 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -78,9 +78,9 @@ class Version(ShortVersion): super().__init__(version, major, minor, patch) split = version.split(".") if len(split) >= 6: - self.country = split[3] + self.country: str = split[3] self.build = int(split[4]) - self.role = split[5] + self.role: str = split[5] else: self.country = country From 2a6842db245745d7b2167235466e52c03802bf4e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 17:03:19 -0500 Subject: [PATCH 082/589] remove db old-to-new migration --- core/data/database.py | 1614 ----------------------------------------- 1 file changed, 1614 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 70fc3e0..65b01aa 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -138,1617 +138,3 @@ class Data: return None self.logger.info(f"Successfully migrated {game} to schema version {version}") - - def dump_db(self): - dbname = self.config.database.name - - self.logger.info("Database dumper for use with the reworked schema") - self.logger.info("Dumping users...") - - sql = f"SELECT * FROM `{dbname}`.`user`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - users = result.fetchall() - - user_list: List[Dict[str, Any]] = [] - for usr in users: - user_list.append({ - "id": usr["id"], - "username": usr["username"], - "email": usr["email"], - "password": usr["password"], - "permissions": usr["permissions"], - "created_date": datetime.strftime(usr["created_date"], "%Y-%m-%d %H:%M:%S"), - "last_login_date": datetime.strftime(usr["accessed_date"], "%Y-%m-%d %H:%M:%S"), - }) - - self.logger.info(f"Done, found {len(user_list)} users") - with open("dbdump-user.json", "w", encoding="utf-8") as f: - f.write(json.dumps(user_list)) - self.logger.info(f"Saved as dbdump-user.json") - - self.logger.info("Dumping cards...") - - sql = f"SELECT * FROM `{dbname}`.`card`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - cards = result.fetchall() - - card_list: List[Dict[str, Any]] = [] - for crd in cards: - card_list.append({ - "id": crd["id"], - "user": crd["user"], - "access_code": crd["access_code"], - "is_locked": crd["is_locked"], - "is_banned": crd["is_banned"], - "created_date": datetime.strftime(crd["created_date"], "%Y-%m-%d %H:%M:%S"), - "last_login_date": datetime.strftime(crd["accessed_date"], "%Y-%m-%d %H:%M:%S"), - }) - - self.logger.info(f"Done, found {len(card_list)} cards") - with open("dbdump-card.json", "w", encoding="utf-8") as f: - f.write(json.dumps(card_list)) - self.logger.info(f"Saved as dbdump-card.json") - - self.logger.info("Dumping arcades...") - - sql = f"SELECT * FROM `{dbname}`.`arcade`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - arcades = result.fetchall() - - arcade_list: List[Dict[str, Any]] = [] - for arc in arcades: - arcade_list.append({ - "id": arc["id"], - "name": arc["name"], - "nickname": arc["name"], - "country": None, - "country_id": None, - "state": None, - "city": None, - "region_id": None, - "timezone": None, - }) - - self.logger.info(f"Done, found {len(arcade_list)} arcades") - with open("dbdump-arcade.json", "w", encoding="utf-8") as f: - f.write(json.dumps(arcade_list)) - self.logger.info(f"Saved as dbdump-arcade.json") - - self.logger.info("Dumping machines...") - - sql = f"SELECT * FROM `{dbname}`.`machine`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - machines = result.fetchall() - - machine_list: List[Dict[str, Any]] = [] - for mech in machines: - if "country" in mech["data"]: - country = mech["data"]["country"] - else: - country = None - - if "ota_enable" in mech["data"]: - ota_enable = mech["data"]["ota_enable"] - else: - ota_enable = None - - machine_list.append({ - "id": mech["id"], - "arcade": mech["arcade"], - "serial": mech["keychip"], - "game": mech["game"], - "board": None, - "country": country, - "timezone": None, - "ota_enable": ota_enable, - "is_cab": False, - }) - - self.logger.info(f"Done, found {len(machine_list)} machines") - with open("dbdump-machine.json", "w", encoding="utf-8") as f: - f.write(json.dumps(machine_list)) - self.logger.info(f"Saved as dbdump-machine.json") - - self.logger.info("Dumping arcade owners...") - - sql = f"SELECT * FROM `{dbname}`.`arcade_owner`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - arcade_owners = result.fetchall() - - owner_list: List[Dict[str, Any]] = [] - for owner in owner_list: - owner_list.append(owner._asdict()) - - self.logger.info(f"Done, found {len(owner_list)} arcade owners") - with open("dbdump-arcade_owner.json", "w", encoding="utf-8") as f: - f.write(json.dumps(owner_list)) - self.logger.info(f"Saved as dbdump-arcade_owner.json") - - self.logger.info("Dumping profiles...") - - sql = f"SELECT * FROM `{dbname}`.`profile`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - profiles = result.fetchall() - - profile_list: Dict[List[Dict[str, Any]]] = {} - for pf in profiles: - game = pf["game"] - - if game not in profile_list: - profile_list[game] = [] - - profile_list[game].append({ - "id": pf["id"], - "user": pf["user"], - "version": pf["version"], - "use_count": pf["use_count"], - "name": pf["name"], - "game_id": pf["game_id"], - "mods": pf["mods"], - "data": pf["data"], - }) - - self.logger.info(f"Done, found profiles for {len(profile_list)} games") - with open("dbdump-profile.json", "w", encoding="utf-8") as f: - f.write(json.dumps(profile_list)) - self.logger.info(f"Saved as dbdump-profile.json") - - self.logger.info("Dumping scores...") - - sql = f"SELECT * FROM `{dbname}`.`score`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - scores = result.fetchall() - - score_list: Dict[List[Dict[str, Any]]] = {} - for sc in scores: - game = sc["game"] - - if game not in score_list: - score_list[game] = [] - - score_list[game].append({ - "id": sc["id"], - "user": sc["user"], - "version": sc["version"], - "song_id": sc["song_id"], - "chart_id": sc["chart_id"], - "score1": sc["score1"], - "score2": sc["score2"], - "fc1": sc["fc1"], - "fc2": sc["fc2"], - "cleared": sc["cleared"], - "grade": sc["grade"], - "data": sc["data"], - }) - - self.logger.info(f"Done, found scores for {len(score_list)} games") - with open("dbdump-score.json", "w", encoding="utf-8") as f: - f.write(json.dumps(score_list)) - self.logger.info(f"Saved as dbdump-score.json") - - self.logger.info("Dumping achievements...") - - sql = f"SELECT * FROM `{dbname}`.`achievement`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - achievements = result.fetchall() - - achievement_list: Dict[List[Dict[str, Any]]] = {} - for ach in achievements: - game = ach["game"] - - if game not in achievement_list: - achievement_list[game] = [] - - achievement_list[game].append({ - "id": ach["id"], - "user": ach["user"], - "version": ach["version"], - "type": ach["type"], - "achievement_id": ach["achievement_id"], - "data": ach["data"], - }) - - self.logger.info(f"Done, found achievements for {len(achievement_list)} games") - with open("dbdump-achievement.json", "w", encoding="utf-8") as f: - f.write(json.dumps(achievement_list)) - self.logger.info(f"Saved as dbdump-achievement.json") - - self.logger.info("Dumping items...") - - sql = f"SELECT * FROM `{dbname}`.`item`" - - result = self.base.execute(sql) - if result is None: - self.logger.error("Failed") - return None - items = result.fetchall() - - item_list: Dict[List[Dict[str, Any]]] = {} - for itm in items: - game = itm["game"] - - if game not in item_list: - item_list[game] = [] - - item_list[game].append({ - "id": itm["id"], - "user": itm["user"], - "version": itm["version"], - "type": itm["type"], - "item_id": itm["item_id"], - "data": ach["data"], - }) - - self.logger.info(f"Done, found items for {len(item_list)} games") - with open("dbdump-item.json", "w", encoding="utf-8") as f: - f.write(json.dumps(item_list)) - self.logger.info(f"Saved as dbdump-item.json") - - def restore_from_old_schema(self): - # Import the tables we expect to be there - from core.data.schema.user import aime_user - from core.data.schema.card import aime_card - from core.data.schema.arcade import arcade, machine, arcade_owner - from sqlalchemy.dialects.mysql import Insert - - # Make sure that all the tables we're trying to access exist - self.create_database() - - # Import the data, making sure that dependencies are accounted for - if os.path.exists("dbdump-user.json"): - users = [] - with open("dbdump-user.json", "r", encoding="utf-8") as f: - users = json.load(f) - - self.logger.info(f"Load {len(users)} users") - - for user in users: - sql = Insert(aime_user).values(**user) - - conflict = sql.on_duplicate_key_update(**user) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert user {user['id']}") - continue - self.logger.info(f"Inserted user {user['id']} -> {result.lastrowid}") - - if os.path.exists("dbdump-card.json"): - cards = [] - with open("dbdump-card.json", "r", encoding="utf-8") as f: - cards = json.load(f) - - self.logger.info(f"Load {len(cards)} cards") - - for card in cards: - sql = Insert(aime_card).values(**card) - - conflict = sql.on_duplicate_key_update(**card) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert card {card['id']}") - continue - self.logger.info(f"Inserted card {card['id']} -> {result.lastrowid}") - - if os.path.exists("dbdump-arcade.json"): - arcades = [] - with open("dbdump-arcade.json", "r", encoding="utf-8") as f: - arcades = json.load(f) - - self.logger.info(f"Load {len(arcades)} arcades") - - for ac in arcades: - sql = Insert(arcade).values(**ac) - - conflict = sql.on_duplicate_key_update(**ac) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert arcade {ac['id']}") - continue - self.logger.info(f"Inserted arcade {ac['id']} -> {result.lastrowid}") - - if os.path.exists("dbdump-arcade_owner.json"): - ac_owners = [] - with open("dbdump-arcade_owner.json", "r", encoding="utf-8") as f: - ac_owners = json.load(f) - - self.logger.info(f"Load {len(ac_owners)} arcade owners") - - for owner in ac_owners: - sql = Insert(arcade_owner).values(**owner) - - conflict = sql.on_duplicate_key_update(**owner) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert arcade_owner {owner['user']}") - continue - self.logger.info(f"Inserted arcade_owner {owner['user']} -> {result.lastrowid}") - - if os.path.exists("dbdump-machine.json"): - mechs = [] - with open("dbdump-machine.json", "r", encoding="utf-8") as f: - mechs = json.load(f) - - self.logger.info(f"Load {len(mechs)} machines") - - for mech in mechs: - sql = Insert(machine).values(**mech) - - conflict = sql.on_duplicate_key_update(**mech) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert machine {mech['id']}") - continue - self.logger.info(f"Inserted machine {mech['id']} -> {result.lastrowid}") - - # Now the fun part, grabbing all our scores, profiles, items, and achievements and trying - # to conform them to our current, freeform schema. This will be painful... - profiles = {} - items = {} - scores = {} - achievements = {} - - if os.path.exists("dbdump-profile.json"): - with open("dbdump-profile.json", "r", encoding="utf-8") as f: - profiles = json.load(f) - - self.logger.info(f"Load {len(profiles)} profiles") - - if os.path.exists("dbdump-item.json"): - with open("dbdump-item.json", "r", encoding="utf-8") as f: - items = json.load(f) - - self.logger.info(f"Load {len(items)} items") - - if os.path.exists("dbdump-score.json"): - with open("dbdump-score.json", "r", encoding="utf-8") as f: - scores = json.load(f) - - self.logger.info(f"Load {len(scores)} scores") - - if os.path.exists("dbdump-achievement.json"): - with open("dbdump-achievement.json", "r", encoding="utf-8") as f: - achievements = json.load(f) - - self.logger.info(f"Load {len(achievements)} achievements") - - # Chuni / Chusan - if os.path.exists("titles/chuni/schema"): - from titles.chuni.schema.item import character, item, duel, map, map_area - from titles.chuni.schema.profile import profile, profile_ex, option, option_ex - from titles.chuni.schema.profile import recent_rating, activity, charge, emoney - from titles.chuni.schema.profile import overpower - from titles.chuni.schema.score import best_score, course - - chuni_profiles = [] - chuni_items = [] - chuni_scores = [] - - if "SDBT" in profiles: - chuni_profiles = profiles["SDBT"] - if "SDBT" in items: - chuni_items = items["SDBT"] - if "SDBT" in scores: - chuni_scores = scores["SDBT"] - if "SDHD" in profiles: - chuni_profiles += profiles["SDHD"] - if "SDHD" in items: - chuni_items += items["SDHD"] - if "SDHD" in scores: - chuni_scores += scores["SDHD"] - - self.logger.info(f"Importing {len(chuni_profiles)} chunithm/chunithm new profiles") - - for pf in chuni_profiles: - if type(pf["data"]) is not dict: - pf["data"] = json.loads(pf["data"]) - pf_data = pf["data"] - - # data - if "userData" in pf_data: - pf_data["userData"]["userName"] = bytes([ord(c) for c in pf_data["userData"]["userName"]]).decode("utf-8") - pf_data["userData"]["user"] = pf["user"] - pf_data["userData"]["version"] = pf["version"] - pf_data["userData"].pop("accessCode") - - if pf_data["userData"]["lastRomVersion"].startswith("2."): - pf_data["userData"]["version"] += 10 - - pf_data["userData"] = self.base.fix_bools(pf_data["userData"]) - - sql = Insert(profile).values(**pf_data["userData"]) - conflict = sql.on_duplicate_key_update(**pf_data["userData"]) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile data for {pf['user']}") - continue - self.logger.info(f"Inserted chuni profile for {pf['user']} ->{result.lastrowid}") - - # data_ex - if "userDataEx" in pf_data and len(pf_data["userDataEx"]) > 0: - pf_data["userDataEx"][0]["user"] = pf["user"] - pf_data["userDataEx"][0]["version"] = pf["version"] - - pf_data["userDataEx"] = self.base.fix_bools(pf_data["userDataEx"][0]) - - sql = Insert(profile_ex).values(**pf_data["userDataEx"]) - conflict = sql.on_duplicate_key_update(**pf_data["userDataEx"]) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile data_ex for {pf['user']}") - continue - self.logger.info(f"Inserted chuni profile data_ex for {pf['user']} ->{result.lastrowid}") - - # option - if "userGameOption" in pf_data: - pf_data["userGameOption"]["user"] = pf["user"] - - pf_data["userGameOption"] = self.base.fix_bools(pf_data["userGameOption"]) - - sql = Insert(option).values(**pf_data["userGameOption"]) - conflict = sql.on_duplicate_key_update(**pf_data["userGameOption"]) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile options for {pf['user']}") - continue - self.logger.info(f"Inserted chuni profile options for {pf['user']} ->{result.lastrowid}") - - # option_ex - if "userGameOptionEx" in pf_data and len(pf_data["userGameOptionEx"]) > 0: - pf_data["userGameOptionEx"][0]["user"] = pf["user"] - - pf_data["userGameOptionEx"] = self.base.fix_bools(pf_data["userGameOptionEx"][0]) - - sql = Insert(option_ex).values(**pf_data["userGameOptionEx"]) - conflict = sql.on_duplicate_key_update(**pf_data["userGameOptionEx"]) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile option_ex for {pf['user']}") - continue - self.logger.info(f"Inserted chuni profile option_ex for {pf['user']} ->{result.lastrowid}") - - # recent_rating - if "userRecentRatingList" in pf_data: - rr = { - "user": pf["user"], - "recentRating": pf_data["userRecentRatingList"] - } - - sql = Insert(recent_rating).values(**rr) - conflict = sql.on_duplicate_key_update(**rr) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile recent_rating for {pf['user']}") - continue - self.logger.info(f"Inserted chuni profile recent_rating for {pf['user']} ->{result.lastrowid}") - - # activity - if "userActivityList" in pf_data: - for act in pf_data["userActivityList"]: - act["user"] = pf["user"] - - sql = Insert(activity).values(**act) - conflict = sql.on_duplicate_key_update(**act) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile activity for {pf['user']}") - else: - self.logger.info(f"Inserted chuni profile activity for {pf['user']} ->{result.lastrowid}") - - # charge - if "userChargeList" in pf_data: - for cg in pf_data["userChargeList"]: - cg["user"] = pf["user"] - - cg = self.base.fix_bools(cg) - - sql = Insert(charge).values(**cg) - conflict = sql.on_duplicate_key_update(**cg) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile charge for {pf['user']}") - else: - self.logger.info(f"Inserted chuni profile charge for {pf['user']} ->{result.lastrowid}") - - # emoney - if "userEmoneyList" in pf_data: - for emon in pf_data["userEmoneyList"]: - emon["user"] = pf["user"] - - sql = Insert(emoney).values(**emon) - conflict = sql.on_duplicate_key_update(**emon) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile emoney for {pf['user']}") - else: - self.logger.info(f"Inserted chuni profile emoney for {pf['user']} ->{result.lastrowid}") - - # overpower - if "userOverPowerList" in pf_data: - for op in pf_data["userOverPowerList"]: - op["user"] = pf["user"] - - sql = Insert(overpower).values(**op) - conflict = sql.on_duplicate_key_update(**op) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni profile overpower for {pf['user']}") - else: - self.logger.info(f"Inserted chuni profile overpower for {pf['user']} ->{result.lastrowid}") - - # map_area - if "userMapAreaList" in pf_data: - for ma in pf_data["userMapAreaList"]: - ma["user"] = pf["user"] - - ma = self.base.fix_bools(ma) - - sql = Insert(map_area).values(**ma) - conflict = sql.on_duplicate_key_update(**ma) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni map_area for {pf['user']}") - else: - self.logger.info(f"Inserted chuni map_area for {pf['user']} ->{result.lastrowid}") - - #duel - if "userDuelList" in pf_data: - for ma in pf_data["userDuelList"]: - ma["user"] = pf["user"] - - ma = self.base.fix_bools(ma) - - sql = Insert(duel).values(**ma) - conflict = sql.on_duplicate_key_update(**ma) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni duel for {pf['user']}") - else: - self.logger.info(f"Inserted chuni duel for {pf['user']} ->{result.lastrowid}") - - # map - if "userMapList" in pf_data: - for ma in pf_data["userMapList"]: - ma["user"] = pf["user"] - - ma = self.base.fix_bools(ma) - - sql = Insert(map).values(**ma) - conflict = sql.on_duplicate_key_update(**ma) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni map for {pf['user']}") - else: - self.logger.info(f"Inserted chuni map for {pf['user']} ->{result.lastrowid}") - - self.logger.info(f"Importing {len(chuni_items)} chunithm/chunithm new items") - - for i in chuni_items: - if type(i["data"]) is not dict: - i["data"] = json.loads(i["data"]) - i_data = i["data"] - - i_data["user"] = i["user"] - - i_data = self.base.fix_bools(i_data) - - try: i_data.pop("assignIllust") - except: pass - - try: i_data.pop("exMaxLv") - except: pass - - if i["type"] == 20: #character - sql = Insert(character).values(**i_data) - else: - sql = Insert(item).values(**i_data) - - conflict = sql.on_duplicate_key_update(**i_data) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert chuni item for user {i['user']}") - - else: - self.logger.info(f"Inserted chuni item for user {i['user']} {i['item_id']} -> {result.lastrowid}") - - self.logger.info(f"Importing {len(chuni_scores)} chunithm/chunithm new scores") - - for sc in chuni_scores: - if type(sc["data"]) is not dict: - sc["data"] = json.loads(sc["data"]) - - score_data = self.base.fix_bools(sc["data"]) - - try: score_data.pop("theoryCount") - except: pass - - try: score_data.pop("ext1") - except: pass - - score_data["user"] = sc["user"] - - sql = Insert(best_score).values(**score_data) - conflict = sql.on_duplicate_key_update(**score_data) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to put chuni score for user {sc['user']}") - else: - self.logger.info(f"Inserted chuni score for user {sc['user']} {sc['song_id']}/{sc['chart_id']} -> {result.lastrowid}") - - else: - self.logger.info(f"Chuni/Chusan not found, skipping...") - - # CXB - if os.path.exists("titles/cxb/schema"): - from titles.cxb.schema.item import energy - from titles.cxb.schema.profile import profile - from titles.cxb.schema.score import score, ranking - - cxb_profiles = [] - cxb_items = [] - cxb_scores = [] - - if "SDCA" in profiles: - cxb_profiles = profiles["SDCA"] - if "SDCA" in items: - cxb_items = items["SDCA"] - if "SDCA" in scores: - cxb_scores = scores["SDCA"] - - self.logger.info(f"Importing {len(cxb_profiles)} CXB profiles") - - for pf in cxb_profiles: - user = pf["user"] - version = pf["version"] - pf_data = pf["data"]["data"] - pf_idx = pf["data"]["index"] - - for x in range(len(pf_data)): - sql = Insert(profile).values( - user = user, - version = version, - index = int(pf_idx[x]), - data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] - ) - - conflict = sql.on_duplicate_key_update( - user = user, - version = version, - index = int(pf_idx[x]), - data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert CXB profile for user {user} Index {pf_idx[x]}") - - self.logger.info(f"Importing {len(cxb_scores)} CXB scores") - - for sc in cxb_scores: - user = sc["user"] - version = sc["version"] - mcode = sc["data"]["mcode"] - index = sc["data"]["index"] - - sql = Insert(score).values( - user = user, - game_version = version, - song_mcode = mcode, - song_index = index, - data = sc["data"] - ) - - conflict = sql.on_duplicate_key_update( - user = user, - game_version = version, - song_mcode = mcode, - song_index = index, - data = sc["data"] - ) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert CXB score for user {user} mcode {mcode}") - - self.logger.info(f"Importing {len(cxb_items)} CXB items") - - for it in cxb_items: - user = it["user"] - - if it["type"] == 3: # energy - sql = Insert(energy).values( - user = user, - energy = it["data"]["total"] - ) - - conflict = sql.on_duplicate_key_update( - user = user, - energy = it["data"]["total"] - ) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert CXB energy for user {user}") - - elif it["type"] == 2: - sql = Insert(ranking).values( - user = user, - rev_id = it["data"]["rid"], - song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, - score = it["data"]["sc"][0], - clear = it["data"]["clear"], - ) - - conflict = sql.on_duplicate_key_update( - user = user, - rev_id = it["data"]["rid"], - song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, - score = it["data"]["sc"][0], - clear = it["data"]["clear"], - ) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert CXB ranking for user {user}") - - else: - self.logger.error(f"Unknown CXB item type {it['type']} for user {user}") - - else: - self.logger.info(f"CXB not found, skipping...") - - # Diva - if os.path.exists("titles/diva/schema"): - from titles.diva.schema.profile import profile - from titles.diva.schema.score import score - from titles.diva.schema.item import shop - - diva_profiles = [] - diva_scores = [] - - if "SBZV" in profiles: - diva_profiles = profiles["SBZV"] - if "SBZV" in scores: - diva_scores = scores["SBZV"] - - self.logger.info(f"Importing {len(diva_profiles)} Diva profiles") - - for pf in diva_profiles: - pf["data"]["user"] = pf["user"] - pf["data"]["version"] = pf["version"] - pf_data = pf["data"] - - if "mdl_eqp_ary" in pf["data"]: - sql = Insert(shop).values( - user = user, - version = version, - mdl_eqp_ary = pf["data"]["mdl_eqp_ary"], - ) - conflict = sql.on_duplicate_key_update( - user = user, - version = version, - mdl_eqp_ary = pf["data"]["mdl_eqp_ary"] - ) - self.base.execute(conflict) - pf["data"].pop("mdl_eqp_ary") - - sql = Insert(profile).values(**pf_data) - conflict = sql.on_duplicate_key_update(**pf_data) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert diva profile for {pf['user']}") - - self.logger.info(f"Importing {len(diva_scores)} Diva scores") - - for sc in diva_scores: - user = sc["user"] - - clr_kind = -1 - for x in sc["data"]["stg_clr_kind"].split(","): - if x != "-1": - clr_kind = x - - cool_ct = 0 - for x in sc["data"]["stg_cool_cnt"].split(","): - if x != "0": - cool_ct = x - - fine_ct = 0 - for x in sc["data"]["stg_fine_cnt"].split(","): - if x != "0": - fine_ct = x - - safe_ct = 0 - for x in sc["data"]["stg_safe_cnt"].split(","): - if x != "0": - safe_ct = x - - sad_ct = 0 - for x in sc["data"]["stg_sad_cnt"].split(","): - if x != "0": - sad_ct = x - - worst_ct = 0 - for x in sc["data"]["stg_wt_wg_cnt"].split(","): - if x != "0": - worst_ct = x - - max_cmb = 0 - for x in sc["data"]["stg_max_cmb"].split(","): - if x != "0": - max_cmb = x - - sql = Insert(score).values( - user = user, - version = sc["version"], - pv_id = sc["song_id"], - difficulty = sc["chart_id"], - score = sc["score1"], - atn_pnt = sc["score2"], - clr_kind = clr_kind, - sort_kind = sc["data"]["sort_kind"], - cool = cool_ct, - fine = fine_ct, - safe = safe_ct, - sad = sad_ct, - worst = worst_ct, - max_combo = max_cmb, - ) - - conflict = sql.on_duplicate_key_update(user = user, - version = sc["version"], - pv_id = sc["song_id"], - difficulty = sc["chart_id"], - score = sc["score1"], - atn_pnt = sc["score2"], - clr_kind = clr_kind, - sort_kind = sc["data"]["sort_kind"], - cool = cool_ct, - fine = fine_ct, - safe = safe_ct, - sad = sad_ct, - worst = worst_ct, - max_combo = max_cmb - ) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert diva score for {pf['user']}") - - else: - self.logger.info(f"Diva not found, skipping...") - - # Ongeki - if os.path.exists("titles/ongeki/schema"): - from titles.ongeki.schema.item import card, deck, character, boss, story - from titles.ongeki.schema.item import chapter, item, music_item, login_bonus - from titles.ongeki.schema.item import event_point, mission_point, scenerio - from titles.ongeki.schema.item import trade_item, event_music, tech_event - from titles.ongeki.schema.profile import profile, option, activity, recent_rating - from titles.ongeki.schema.profile import rating_log, training_room, kop - from titles.ongeki.schema.score import score_best, tech_count, playlog - from titles.ongeki.schema.log import session_log - - item_types = { - "character": 20, - "story": 21, - "card": 22, - "deck": 23, - "login": 24, - "chapter": 25 - } - - ongeki_profiles = [] - ongeki_items = [] - ongeki_scores = [] - - if "SDDT" in profiles: - ongeki_profiles = profiles["SDDT"] - if "SDDT" in items: - ongeki_items = items["SDDT"] - if "SDDT" in scores: - ongeki_scores = scores["SDDT"] - - self.logger.info(f"Importing {len(ongeki_profiles)} ongeki profiles") - - for pf in ongeki_profiles: - user = pf["user"] - version = pf["version"] - pf_data = pf["data"] - - pf_data["userData"]["user"] = user - pf_data["userData"]["version"] = version - pf_data["userData"].pop("accessCode") - - sql = Insert(profile).values(**pf_data["userData"]) - conflict = sql.on_duplicate_key_update(**pf_data["userData"]) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile data for user {pf['user']}") - continue - - pf_data["userOption"]["user"] = user - - sql = Insert(option).values(**pf_data["userOption"]) - conflict = sql.on_duplicate_key_update(**pf_data["userOption"]) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile options for user {pf['user']}") - continue - - for pf_list in pf_data["userActivityList"]: - pf_list["user"] = user - - sql = Insert(activity).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile activity for user {pf['user']}") - continue - - sql = Insert(recent_rating).values( - user = user, - recentRating = pf_data["userRecentRatingList"] - ) - - conflict = sql.on_duplicate_key_update( - user = user, - recentRating = pf_data["userRecentRatingList"] - ) - result = self.base.execute(conflict) - - if result is None: - self.logger.error(f"Failed to insert ongeki profile recent rating for user {pf['user']}") - continue - - for pf_list in pf_data["userRatinglogList"]: - pf_list["user"] = user - - sql = Insert(rating_log).values(**pf_list) - - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile rating log for user {pf['user']}") - continue - - for pf_list in pf_data["userTrainingRoomList"]: - pf_list["user"] = user - - sql = Insert(training_room).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") - continue - - if "userKopList" in pf_data: - for pf_list in pf_data["userKopList"]: - pf_list["user"] = user - - sql = Insert(kop).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") - continue - - for pf_list in pf_data["userBossList"]: - pf_list["user"] = user - - sql = Insert(boss).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item boss for user {pf['user']}") - continue - - for pf_list in pf_data["userDeckList"]: - pf_list["user"] = user - - sql = Insert(deck).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item deck for user {pf['user']}") - continue - - for pf_list in pf_data["userStoryList"]: - pf_list["user"] = user - - sql = Insert(story).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item story for user {pf['user']}") - continue - - for pf_list in pf_data["userChapterList"]: - pf_list["user"] = user - - sql = Insert(chapter).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item chapter for user {pf['user']}") - continue - - for pf_list in pf_data["userPlaylogList"]: - pf_list["user"] = user - - sql = Insert(playlog).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki score playlog for user {pf['user']}") - continue - - for pf_list in pf_data["userMusicItemList"]: - pf_list["user"] = user - - sql = Insert(music_item).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item music item for user {pf['user']}") - continue - - for pf_list in pf_data["userTechCountList"]: - pf_list["user"] = user - - sql = Insert(tech_count).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item tech count for user {pf['user']}") - continue - - if "userTechEventList" in pf_data: - for pf_list in pf_data["userTechEventList"]: - pf_list["user"] = user - - sql = Insert(tech_event).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item tech event for user {pf['user']}") - continue - - for pf_list in pf_data["userTradeItemList"]: - pf_list["user"] = user - - sql = Insert(trade_item).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item trade item for user {pf['user']}") - continue - - if "userEventMusicList" in pf_data: - for pf_list in pf_data["userEventMusicList"]: - pf_list["user"] = user - - sql = Insert(event_music).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item event music for user {pf['user']}") - continue - - if "userEventPointList" in pf_data: - for pf_list in pf_data["userEventPointList"]: - pf_list["user"] = user - - sql = Insert(event_point).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item event point for user {pf['user']}") - continue - - for pf_list in pf_data["userLoginBonusList"]: - pf_list["user"] = user - - sql = Insert(login_bonus).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item login bonus for user {pf['user']}") - continue - - for pf_list in pf_data["userMissionPointList"]: - pf_list["user"] = user - - sql = Insert(mission_point).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item mission point for user {pf['user']}") - continue - - for pf_list in pf_data["userScenarioList"]: - pf_list["user"] = user - - sql = Insert(scenerio).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item scenerio for user {pf['user']}") - continue - - if "userSessionlogList" in pf_data: - for pf_list in pf_data["userSessionlogList"]: - pf_list["user"] = user - - sql = Insert(session_log).values(**pf_list) - conflict = sql.on_duplicate_key_update(**pf_list) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki log session for user {pf['user']}") - continue - - self.logger.info(f"Importing {len(ongeki_items)} ongeki items") - - for it in ongeki_items: - user = it["user"] - it_type = it["type"] - it_id = it["item_id"] - it_data = it["data"] - it_data["user"] = user - - if it_type == item_types["character"] and "characterId" in it_data: - sql = Insert(character).values(**it_data) - - elif it_type == item_types["story"]: - sql = Insert(story).values(**it_data) - - elif it_type == item_types["card"]: - sql = Insert(card).values(**it_data) - - elif it_type == item_types["deck"]: - sql = Insert(deck).values(**it_data) - - elif it_type == item_types["login"]: # login bonus - sql = Insert(login_bonus).values(**it_data) - - elif it_type == item_types["chapter"]: - sql = Insert(chapter).values(**it_data) - - else: - sql = Insert(item).values(**it_data) - - conflict = sql.on_duplicate_key_update(**it_data) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki item {it_id} kind {it_type} for user {user}") - - self.logger.info(f"Importing {len(ongeki_scores)} ongeki scores") - - for sc in ongeki_scores: - user = sc["user"] - sc_data = sc["data"] - sc_data["user"] = user - - sql = Insert(score_best).values(**sc_data) - conflict = sql.on_duplicate_key_update(**sc_data) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert ongeki score for user {user}: {sc['song_id']}/{sc['chart_id']}") - - else: - self.logger.info(f"Ongeki not found, skipping...") - - # Wacca - if os.path.exists("titles/wacca/schema"): - from titles.wacca.schema.profile import profile, option, bingo, gate, favorite - from titles.wacca.schema.item import item, ticket, song_unlock, trophy - from titles.wacca.schema.score import best_score, stageup - from titles.wacca.reverse import WaccaReverse - from titles.wacca.const import WaccaConstants - - default_opts = WaccaReverse.OPTIONS_DEFAULTS - opts = WaccaConstants.OPTIONS - item_types = WaccaConstants.ITEM_TYPES - - wacca_profiles = [] - wacca_items = [] - wacca_scores = [] - wacca_achievements = [] - - if "SDFE" in profiles: - wacca_profiles = profiles["SDFE"] - if "SDFE" in items: - wacca_items = items["SDFE"] - if "SDFE" in scores: - wacca_scores = scores["SDFE"] - if "SDFE" in achievements: - wacca_achievements = achievements["SDFE"] - - self.logger.info(f"Importing {len(wacca_profiles)} wacca profiles") - - for pf in wacca_profiles: - if pf["version"] == 0 or pf["version"] == 1: - season = 1 - elif pf["version"] == 2 or pf["version"] == 3: - season = 2 - elif pf["version"] >= 4: - season = 3 - - if type(pf["data"]) is not dict: - pf["data"] = json.loads(pf["data"]) - - try: - sql = Insert(profile).values( - id = pf["id"], - user = pf["user"], - version = pf["version"], - season = season, - username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], - xp = pf["data"]["profile"]["xp"], - xp_season = pf["data"]["profile"]["xp"], - wp = pf["data"]["profile"]["wp"], - wp_season = pf["data"]["profile"]["wp"], - wp_total = pf["data"]["profile"]["total_wp_gained"], - dan_type = pf["data"]["profile"]["dan_type"], - dan_level = pf["data"]["profile"]["dan_level"], - title_0 = pf["data"]["profile"]["title_part_ids"][0], - title_1 = pf["data"]["profile"]["title_part_ids"][1], - title_2 = pf["data"]["profile"]["title_part_ids"][2], - rating = pf["data"]["profile"]["rating"], - vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, - login_count = pf["use_count"], - playcount_single = pf["use_count"], - playcount_single_season = pf["use_count"], - last_game_ver = pf["data"]["profile"]["last_game_ver"], - last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, - last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, - last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, - last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, - last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, - last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), - ) - - conflict = sql.on_duplicate_key_update( - id = pf["id"], - user = pf["user"], - version = pf["version"], - season = season, - username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], - xp = pf["data"]["profile"]["xp"], - xp_season = pf["data"]["profile"]["xp"], - wp = pf["data"]["profile"]["wp"], - wp_season = pf["data"]["profile"]["wp"], - wp_total = pf["data"]["profile"]["total_wp_gained"], - dan_type = pf["data"]["profile"]["dan_type"], - dan_level = pf["data"]["profile"]["dan_level"], - title_0 = pf["data"]["profile"]["title_part_ids"][0], - title_1 = pf["data"]["profile"]["title_part_ids"][1], - title_2 = pf["data"]["profile"]["title_part_ids"][2], - rating = pf["data"]["profile"]["rating"], - vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, - login_count = pf["use_count"], - playcount_single = pf["use_count"], - playcount_single_season = pf["use_count"], - last_game_ver = pf["data"]["profile"]["last_game_ver"], - last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, - last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, - last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, - last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, - last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, - last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), - ) - - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca profile for user {pf['user']}") - continue - - for opt, val in pf["data"]["option"].items(): - if val != default_opts[opt]: - opt_id = opts[opt] - sql = Insert(option).values( - user = pf["user"], - opt_id = opt_id, - value = val, - ) - - conflict = sql.on_duplicate_key_update( - user = pf["user"], - opt_id = opt_id, - value = val, - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca option for user {pf['user']} {opt} -> {val}") - - except KeyError as e: - self.logger.warn(f"Outdated wacca profile, skipping: {e}") - - if "gate" in pf["data"]: - for profile_gate in pf["data"]["gate"]: - sql = Insert(gate).values( - user = pf["user"], - gate_id = profile_gate["id"], - page = profile_gate["page"], - loops = profile_gate["loops"], - progress = profile_gate["progress"], - last_used = datetime.fromtimestamp(profile_gate["last_used"]), - mission_flag = profile_gate["mission_flag"], - total_points = profile_gate["total_points"], - ) - - conflict = sql.on_duplicate_key_update( - user = pf["user"], - gate_id = profile_gate["id"], - page = profile_gate["page"], - loops = profile_gate["loops"], - progress = profile_gate["progress"], - last_used = datetime.fromtimestamp(profile_gate["last_used"]), - mission_flag = profile_gate["mission_flag"], - total_points = profile_gate["total_points"], - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca gate for user {pf['user']} -> {profile_gate['id']}") - continue - - if "favorite" in pf["data"]: - for profile_favorite in pf["data"]["favorite"]: - sql = Insert(favorite).values( - user = pf["user"], - song_id = profile_favorite - ) - - conflict = sql.on_duplicate_key_update( - user = pf["user"], - song_id = profile_favorite - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca favorite songs for user {pf['user']} -> {profile_favorite}") - continue - - for it in wacca_items: - user = it["user"] - item_type = it["type"] - item_id = it["item_id"] - - if type(it["data"]) is not dict: - it["data"] = json.loads(it["data"]) - - if item_type == item_types["ticket"]: - if "quantity" in it["data"]: - for x in range(it["data"]["quantity"]): - sql = Insert(ticket).values( - user = user, - ticket_id = item_id, - ) - - conflict = sql.on_duplicate_key_update( - user = user, - ticket_id = item_id, - ) - result = self.base.execute(conflict) - if result is None: - self.logger.warn(f"Wacca: Failed to insert ticket {item_id} for user {user}") - - elif item_type == item_types["music_unlock"] or item_type == item_types["music_difficulty_unlock"]: - diff = 0 - if "difficulty" in it["data"]: - for x in it["data"]["difficulty"]: - if x == 1: - diff += 1 - else: - break - - sql = Insert(song_unlock).values( - user = user, - song_id = item_id, - highest_difficulty = diff, - ) - - conflict = sql.on_duplicate_key_update( - user = user, - song_id = item_id, - highest_difficulty = diff, - ) - result = self.base.execute(conflict) - if result is None: - self.logger.warn(f"Wacca: Failed to insert song unlock {item_id} {diff} for user {user}") - - elif item_type == item_types["trophy"]: - season = int(item_id / 100000) - sql = Insert(trophy).values( - user = user, - trophy_id = item_id, - season = season, - progress = 0 if "progress" not in it["data"] else it["data"]["progress"], - badge_type = 0 # ??? - ) - - conflict = sql.on_duplicate_key_update( - user = user, - trophy_id = item_id, - season = season, - progress = 0 if "progress" not in it["data"] else it["data"]["progress"], - badge_type = 0 # ??? - ) - result = self.base.execute(conflict) - if result is None: - self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") - - else: - sql = Insert(item).values( - user = user, - item_id = item_id, - type = item_type, - acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), - use_count = it["data"]["uses"] if "uses" in it["data"] else 0, - use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 - ) - - conflict = sql.on_duplicate_key_update( - user = user, - item_id = item_id, - type = item_type, - acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), - use_count = it["data"]["uses"] if "uses" in it["data"] else 0, - use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 - ) - result = self.base.execute(conflict) - if result is None: - self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") - - for sc in wacca_scores: - if type(sc["data"]) is not dict: - sc["data"] = json.loads(sc["data"]) - - sql = Insert(best_score).values( - user = sc["user"], - song_id = int(sc["song_id"]), - chart_id = sc["chart_id"], - score = sc["score1"], - play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], - clear_ct = 1 if sc["cleared"] & 0x01 else 0, - missless_ct = 1 if sc["cleared"] & 0x02 else 0, - fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, - allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, - grade_d_ct = 1 if sc["grade"] & 0x01 else 0, - grade_c_ct = 1 if sc["grade"] & 0x02 else 0, - grade_b_ct = 1 if sc["grade"] & 0x04 else 0, - grade_a_ct = 1 if sc["grade"] & 0x08 else 0, - grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, - grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, - grade_s_ct = 1 if sc["grade"] & 0x40 else 0, - grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, - grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, - grade_master_ct = 1 if sc["grade"] & 0x200 else 0, - grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, - grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, - grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, - best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], - lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], - rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], - ) - - conflict = sql.on_duplicate_key_update( - user = sc["user"], - song_id = int(sc["song_id"]), - chart_id = sc["chart_id"], - score = sc["score1"], - play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], - clear_ct = 1 if sc["cleared"] & 0x01 else 0, - missless_ct = 1 if sc["cleared"] & 0x02 else 0, - fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, - allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, - grade_d_ct = 1 if sc["grade"] & 0x01 else 0, - grade_c_ct = 1 if sc["grade"] & 0x02 else 0, - grade_b_ct = 1 if sc["grade"] & 0x04 else 0, - grade_a_ct = 1 if sc["grade"] & 0x08 else 0, - grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, - grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, - grade_s_ct = 1 if sc["grade"] & 0x40 else 0, - grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, - grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, - grade_master_ct = 1 if sc["grade"] & 0x200 else 0, - grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, - grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, - grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, - best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], - lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], - rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca score for user {sc['user']} {int(sc['song_id'])} {sc['chart_id']}") - - for ach in wacca_achievements: - if ach["version"] == 0 or ach["version"] == 1: - season = 1 - elif ach["version"] == 2 or ach["version"] == 3: - season = 2 - elif ach["version"] >= 4: - season = 3 - - if type(ach["data"]) is not dict: - ach["data"] = json.loads(ach["data"]) - - sql = Insert(stageup).values( - user = ach["user"], - season = season, - stage_id = ach["achievement_id"], - clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], - clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], - song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], - song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], - song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], - play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], - ) - - conflict = sql.on_duplicate_key_update( - user = ach["user"], - season = season, - stage_id = ach["achievement_id"], - clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], - clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], - song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], - song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], - song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], - play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], - ) - result = self.base.execute(conflict) - if result is None: - self.logger.error(f"Failed to insert wacca achievement for user {ach['user']}") - - else: - self.logger.info(f"Wacca not found, skipping...") From 102bf3b5a49d0e8539f7c7cc07bb2667a3d5fd02 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 17:04:26 -0500 Subject: [PATCH 083/589] database: remove functions that no longer exist --- dbutils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/dbutils.py b/dbutils.py index d52128e..4500a13 100644 --- a/dbutils.py +++ b/dbutils.py @@ -32,16 +32,5 @@ if __name__=='__main__': else: data.migrate_database(args.game, int(args.version), args.action) - - elif args.action == "migrate": - data.logger.info("Migrating from old schema to new schema") - data.restore_from_old_schema() - - elif args.action == "dump": - data.logger.info("Dumping old schema to migrate to new schema") - data.dump_db() - - elif args.action == "generate": - pass data.logger.info("Done") From 02e1838d95c2498f438835aa106ad64dcc167232 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 17:05:16 -0500 Subject: [PATCH 084/589] database: add format_serial, validate_keychip_format, set_machine_boardid, set_machine_serial --- core/data/schema/arcade.py | 48 +++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 921e5b5..af4069d 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -4,8 +4,10 @@ from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint from sqlalchemy.types import Integer, String, Boolean from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +import re from core.data.schema.base import BaseData, metadata +from core.const import * arcade = Table( "arcade", @@ -72,20 +74,28 @@ class ArcadeData(BaseData): if result is None: return None return result.fetchone() - def put_machine(self, arcade_id: int, serial: str = None, board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]: + def put_machine(self, arcade_id: int, serial: str = "", board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]: if arcade_id: self.logger.error(f"{__name__ }: Need arcade id!") return None - if serial is None: - pass - sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab) result = self.execute(sql) if result is None: return None return result.lastrowid + def 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) @@ -120,5 +130,31 @@ class ArcadeData(BaseData): if result is None: return None return result.lastrowid - def generate_keychip_serial(self, platform_id: int) -> str: - pass + 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)})") + return False + + platform_code = serial[:4] + platform_rev = serial[4:6] + const_a = serial[6] + num = serial[7:11] + append = serial[11:15] + + if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None: + self.logger.error(f"Serial validate failed: {serial} failed regex") + return False + + if len(append) != 0 or len(append) != 4: + 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}") + return False + + return True From 3acc2dc197a73e96d33d16de744199d2d8cae4b9 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 3 Mar 2023 23:46:29 +0100 Subject: [PATCH 085/589] 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 086/589] 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 087/589] 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 088/589] 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 089/589] 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 090/589] 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 091/589] 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 092/589] 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 093/589] 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 094/589] 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 095/589] 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 096/589] 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 097/589] 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 098/589] 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 099/589] 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 100/589] 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 101/589] 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 102/589] 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 103/589] 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 104/589] 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 105/589] 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 106/589] 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 107/589] 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 108/589] 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 109/589] 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 110/589] 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 111/589] 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 112/589] 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 113/589] 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 114/589] 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 115/589] 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 116/589] 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 117/589] 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 118/589] 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 119/589] 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 120/589] 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 121/589] 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 122/589] 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 123/589] 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 124/589] 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 125/589] 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 126/589] 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 127/589] 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 128/589] 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 129/589] 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 130/589] 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 131/589] 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 132/589] 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 133/589] 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 134/589] 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 135/589] 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 136/589] 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 137/589] 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 138/589] 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 139/589] 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 140/589] 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 141/589] 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 142/589] 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 143/589] 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 144/589] 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 145/589] 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 146/589] 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 147/589] 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, ] From 12fd663eb7c2e7a85269eeac665d7adbaab0bf4e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Mar 2023 23:29:57 -0400 Subject: [PATCH 148/589] wacca: fix songs locking after playing them after unlcoked them with an ex unlock ticket, fixes #12 --- titles/wacca/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 275f380..020c167 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -836,7 +836,7 @@ class WaccaBase: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - + resp.songDetail.lock_state = 1 return resp.make() # TODO: Coop and vs data From ac8a660e1308b2604a6d754da9d6fc21c85877c4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Mar 2023 23:52:33 -0400 Subject: [PATCH 149/589] allnet: allow unknown games to auth in develop mode --- core/allnet.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 2afb8cf..0d4fbf7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -95,14 +95,21 @@ 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.logger.warn(msg) + if not self.config.server.is_develop: + msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + ) + self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = 0 + return self.dict_to_http_form_string([vars(resp)]) + + else: + self.logger.info(f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}") + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + return self.dict_to_http_form_string([vars(resp)]) resp.uri, resp.host = self.uri_registry[req.game_id] From 2a290f2a3df154e39c29d0067f230532b705241c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 24 Mar 2023 18:10:10 +0100 Subject: [PATCH 150/589] chuni: added teams and ticket saving, fixed last played song --- example_config/chuni.yaml | 11 +++++++++ titles/chuni/__init__.py | 2 +- titles/chuni/base.py | 43 ++++++++++++++++++++++++---------- titles/chuni/config.py | 28 ++++++++++++++++++++++ titles/chuni/new.py | 14 ++++------- titles/chuni/newplus.py | 10 ++++---- titles/chuni/schema/profile.py | 6 +++-- titles/chuni/schema/static.py | 17 ++++++-------- 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 3794f06..c70ccab 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,5 +2,16 @@ server: enable: True loglevel: "info" +team: + name: ARTEMiS + +version: + "11": + rom: 2.00.00 + data: 2.00.00 + "12": + rom: 2.05.00 + data: 2.05.00 + crypto: encrypted_only: False \ No newline at end of file diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 7256b10..89cd4f5 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 1 +current_schema_version = 3 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 3668b29..936e5ef 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -32,7 +32,7 @@ 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": []} @@ -130,7 +130,7 @@ class ChuniBase: return { "userId": data["userId"], "length": len(activity_list), - "kind": data["kind"], + "kind": int(data["kind"]), "userActivityList": activity_list, } @@ -451,13 +451,13 @@ class ChuniBase: "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": "1", + "chargeState": 1, "userNameEx": profile["userName"], } 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: + recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) + if recent_rating_list is None: return { "userId": data["userId"], "length": 0, @@ -466,8 +466,8 @@ class ChuniBase: return { "userId": data["userId"], - "length": len(recet_rating_list["recentRating"]), - "userRecentRatingList": recet_rating_list["recentRating"], + "length": len(recent_rating_list["recentRating"]), + "userRecentRatingList": recent_rating_list["recentRating"], } def handle_get_user_region_api_request(self, data: Dict) -> Dict: @@ -479,8 +479,24 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: Team - return {"userId": data["userId"], "teamId": 0} + # TODO: use the database "chuni_profile_team" with a GUI + team_name = self.game_cfg.team.team_name + if team_name == "": + return {"userId": data["userId"], "teamId": 0} + + return { + "userId": data["userId"], + "teamId": 1, + "teamRank": 1, + "teamName": team_name, + "userTeamPoint": { + "userId": data["userId"], + "teamId": 1, + "orderId": 1, + "teamPoint": 1, + "aggrDate": data["playDate"], + }, + } def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { @@ -583,6 +599,9 @@ class ChuniBase: return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + # add tickets after they got bought, this makes sure the tickets are + # still valid after an unsuccessful logout + self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -603,7 +622,5 @@ class ChuniBase: 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 + "userNetBattleData": {"recentNBSelectMusicList": []}, + } diff --git a/titles/chuni/config.py b/titles/chuni/config.py index c4a351f..ac527af 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -21,6 +21,32 @@ class ChuniServerConfig: ) +class ChuniTeamConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def team_name(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "name", default="" + ) + + +class ChuniVersionConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + def version_rom(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" + ) + + def version_data(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" + ) + + class ChuniCryptoConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config @@ -46,4 +72,6 @@ class ChuniCryptoConfig: class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) + self.team = ChuniTeamConfig(self) + self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 611c6d2..2c50c07 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": "2.00.00", - "dataVersion": "2.00.00", + "romVersion": self.game_cfg.version.version_rom(self.version), + "dataVersion": self.game_cfg.version.version_data(self.version), "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", @@ -454,9 +454,7 @@ class ChuniNew(ChuniBase): # 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 + user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} @@ -467,10 +465,6 @@ class ChuniNew(ChuniBase): # 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 - ) + 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 9dec9aa..83debb5 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -1,6 +1,4 @@ -from datetime import datetime, timedelta from typing import Dict, Any -import pytz from core.config import CoreConfig from titles.chuni.new import ChuniNew @@ -15,8 +13,12 @@ class ChuniNewPlus(ChuniNew): 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"]["romVersion"] = self.game_cfg.version.version_rom( + self.version + ) + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( + self.version + ) ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9000b9b..e35769c 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -558,8 +558,10 @@ class ChuniProfileData(BaseData): 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)) + .order_by(activity.c.sortNumber.desc()) # to get the last played track ) result = self.execute(sql) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0784872..4882046 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -390,20 +390,17 @@ class ChuniStaticData(BaseData): return None return result.fetchall() - def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + 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() + 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 - )) + 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: From 6489e3ca21b2e3473ead2e029b80b19dc5c453e2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 26 Mar 2023 04:33:53 -0400 Subject: [PATCH 151/589] pokken: add skeleton LoadUser response --- titles/pokken/base.py | 92 +++++++++++++++++++++++++++++++++++++++++- titles/pokken/index.py | 1 - 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 6c2bf26..1a9d5da 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta import json, logging from typing import Any, Dict +import random -from core.config import CoreConfig +from core.data import Data +from core import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.proto import jackal_pb2 @@ -13,6 +15,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") + self.data = Data(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -123,7 +126,94 @@ class PokkenBase: ranking.event_end = True ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) + return res.SerializeToString() + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_USER + access_code = request.load_user.access_code + user_id = self.data.card.get_user_id_from_card(access_code) + + if user_id is None: # TODO: Toggle auto-register + user_id = self.data.user.create_user() + card_id = self.data.card.create_card(user_id, access_code) + + self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") + + # TODO: Check for user data. For now just treat ever card-in as a new user + + load_usr = jackal_pb2.LoadUserResponseData() + load_usr.commidserv_result = 1 + load_usr.load_hash = 1 + load_usr.cardlock_status = False + load_usr.banapass_id = user_id + load_usr.access_code = access_code + load_usr.new_card_flag = True + load_usr.precedent_release_flag = 0xFFFFFFFF + load_usr.navi_newbie_flag = True + load_usr.navi_enable_flag = True + load_usr.pad_vibrate_flag = True + load_usr.home_region_code = 0 + load_usr.home_loc_name = "" + load_usr.pref_code = 0 + load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) + load_usr.trainer_rank_point = 0 + load_usr.wallet = 0 + load_usr.fight_money = 0 + load_usr.score_point = 0 + load_usr.grade_max_num = 0 + load_usr.total_play_days = 0 + load_usr.play_date_time = 0 + load_usr.lucky_box_fail_num = 0 + load_usr.event_reward_get_flag = 0 + load_usr.rank_pvp_all = 0 + load_usr.rank_pvp_loc = 0 + load_usr.rank_cpu_all = 0 + load_usr.rank_cpu_loc = 0 + load_usr.rank_event = 0 + load_usr.awake_num = 0 + load_usr.use_support_num = 0 + load_usr.rankmatch_flag = 0 + load_usr.title_text_id = 0 + load_usr.title_plate_id = 0 + load_usr.title_decoration_id = 0 + load_usr.navi_trainer = 0 + load_usr.navi_version_id = 0 + load_usr.aid_skill = 0 + load_usr.comment_text_id = 0 + load_usr.comment_word_id = 0 + load_usr.latest_use_pokemon = 0 + load_usr.ex_ko_num = 0 + load_usr.wko_num = 0 + load_usr.timeup_win_num = 0 + load_usr.cool_ko_num = 0 + load_usr.perfect_ko_num = 0 + load_usr.record_flag = 0 + load_usr.site_register_status = 0 + load_usr.continue_num = 0 + load_usr.event_state = 0 + load_usr.event_id = 0 + load_usr.sp_bonus_category_id_1 = 0 + load_usr.sp_bonus_key_value_1 = 0 + load_usr.sp_bonus_category_id_2 = 0 + load_usr.sp_bonus_key_value_2 = 0 + + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 42a5a0e..a7a328f 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -124,7 +124,6 @@ class PokkenServlet(resource.Resource): self.logger.debug(pokken_request) ret = handler(pokken_request) - self.logger.debug(f"Response: {ret}") return ret def handle_matching(self, request: Request) -> bytes: From 541fe76a7c5b02b2b06de99aaf14fa5ce565eebd Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 01:09:16 +0200 Subject: [PATCH 152/589] cardmaker: fixed chuni endless loading --- titles/chuni/new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2c50c07..2963850 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -269,10 +269,10 @@ class ChuniNew(ChuniBase): tmp = user_print_list[x]._asdict() print_list.append(tmp["cardId"]) - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: break - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: next_idx = next_idx + max_ct else: next_idx = -1 From 1aa92458f447b36bfef4e52ddf0547728f136383 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:28:57 +0200 Subject: [PATCH 153/589] chuni: added login bonus (+importer), fixed config strings --- example_config/chuni.yaml | 7 +- titles/chuni/base.py | 133 +++++++++++++++++++++++++++----- titles/chuni/config.py | 27 +++++-- titles/chuni/new.py | 4 +- titles/chuni/newplus.py | 8 +- titles/chuni/read.py | 74 ++++++++++++++++++ titles/chuni/schema/item.py | 75 ++++++++++++++++-- titles/chuni/schema/static.py | 140 ++++++++++++++++++++++++++++++++++ 8 files changed, 424 insertions(+), 44 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index c70ccab..bbac976 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -5,11 +5,14 @@ server: team: name: ARTEMiS +mods: + use_login_bonus: True + version: - "11": + 11: rom: 2.00.00 data: 2.00.00 - "12": + 12: rom: 2.05.00 data: 2.05.00 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 936e5ef..95c35e8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -23,7 +23,92 @@ 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"]}) + """ + Handles the login bonus logic, required for the game because + getUserLoginBonus gets called after getUserItem and therefore the + items needs to be inserted in the database before they get requested. + + Adds a bonusCount after a user logged in after 24 hours, makes sure + loginBonus 30 gets looped, only show the login banner every 24 hours, + adds the bonus to items (itemKind 6) + """ + + # ignore the login bonus if disabled in config + if not self.game_cfg.mods.use_login_bonus: + return {"returnCode": 1} + + user_id = data["userId"] + login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) + + for preset in login_bonus_presets: + # check if a user already has some pogress and if not add the + # login bonus entry + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + if user_login_bonus is None: + self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + # yeah i'm lazy + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + + # skip the login bonus entirely if its already finished + if user_login_bonus["isFinished"]: + continue + + # make sure the last login is more than 24 hours ago + if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta( + hours=24 + ): + # increase the login day counter and update the last login date + bonus_count = user_login_bonus["bonusCount"] + 1 + last_update_date = datetime.now() + + all_login_boni = self.data.static.get_login_bonus( + self.version, preset["id"] + ) + + # assume its not None + max_needed_days = all_login_boni[0]["needLoginDayCount"] + + # make sure to not show login boni after all days got redeemed + is_finished = False + if bonus_count > max_needed_days: + # assume that all login preset ids under 3000 needs to be + # looped, like 30 and 40 are looped, 40 does not work? + if preset["id"] < 3000: + bonus_count = 1 + else: + is_finished = True + + # grab the item for the corresponding day + login_item = self.data.static.get_login_bonus_by_required_days( + self.version, preset["id"], bonus_count + ) + if login_item is not None: + # now add the present to the database so the + # handle_get_user_item_api_request can grab them + self.data.item.put_item( + user_id, + { + "itemId": login_item["presentId"], + "itemKind": 6, + "stock": login_item["itemNum"], + "isValid": True, + }, + ) + + self.data.item.put_login_bonus( + user_id, + self.version, + preset["id"], + bonusCount=bonus_count, + lastUpdateDate=last_update_date, + isWatched=False, + isFinished=is_finished, + ) + return {"returnCode": 1} def handle_game_logout_api_request(self, data: Dict) -> Dict: @@ -309,26 +394,28 @@ class ChuniBase: } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - """ - Unsure how to get this to trigger... - """ + user_id = data["userId"] + user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) + if user_login_bonus is None: + return {"userId": user_id, "length": 0, "userLoginBonusList": []} + + user_login_list = [] + for bonus in user_login_bonus: + user_login_list.append( + { + "presetId": bonus["presetId"], + "bonusCount": bonus["bonusCount"], + "lastUpdateDate": datetime.strftime( + bonus["lastUpdateDate"], "%Y-%m-%d %H:%M:%S" + ), + "isWatched": bonus["isWatched"], + } + ) + return { - "userId": data["userId"], - "length": 2, - "userLoginBonusList": [ - { - "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", - }, - ], + "userId": user_id, + "length": len(user_login_list), + "userLoginBonusList": user_login_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: @@ -596,6 +683,12 @@ class ChuniBase: for emoney in upsert["userEmoneyList"]: self.data.profile.put_profile_emoney(user_id, emoney) + if "userLoginBonusList" in upsert: + for login in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, self.version, login["presetId"], isWatched=True + ) + return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: diff --git a/titles/chuni/config.py b/titles/chuni/config.py index ac527af..48d70d2 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -32,19 +32,29 @@ class ChuniTeamConfig: ) +class ChuniModsConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def use_login_bonus(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "mods", "use_login_bonus", default=True + ) + + class ChuniVersionConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config - def version_rom(self, version: int) -> str: + def version(self, version: int) -> Dict: + """ + in the form of: + 11: {"rom": 2.00.00, "data": 2.00.00} + """ return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" - ) - - def version_data(self, version: int) -> str: - return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" - ) + self.__config, "chuni", "version", default={} + )[version] class ChuniCryptoConfig: @@ -73,5 +83,6 @@ class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) self.team = ChuniTeamConfig(self) + self.mods = ChuniModsConfig(self) self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2963850..67b6fcc 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": self.game_cfg.version.version_rom(self.version), - "dataVersion": self.game_cfg.version.version_data(self.version), + "romVersion": self.game_cfg.version.version(self.version)["rom"], + "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 83debb5..422d57a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -13,12 +13,8 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = self.game_cfg.version.version_rom( - self.version - ) - ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( - self.version - ) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/read.py b/titles/chuni/read.py index abf78e3..9ac13c4 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -42,6 +42,80 @@ class ChuniReader(BaseReader): self.read_music(f"{dir}/music") self.read_charges(f"{dir}/chargeItem") self.read_avatar(f"{dir}/avatarAccessory") + self.read_login_bonus(f"{dir}/") + + def read_login_bonus(self, root_dir: str) -> None: + for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): + for dir in dirs: + if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): + with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: + bytedata = fp.read() + 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 + is_enabled = ( + True if xml_root.find("disableFlag").text == "false" else False + ) + + result = self.data.static.put_login_bonus_preset( + self.version, id, name, is_enabled + ) + + if result is not None: + self.logger.info(f"Inserted login bonus preset {id}") + else: + self.logger.warn(f"Failed to insert login bonus preset {id}") + + for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"): + for name in bonus.findall("loginBonusName"): + bonus_id = name.find("id").text + bonus_name = name.find("str").text + + if path.exists( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml" + ): + with open( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml", + "rb", + ) as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + bonus_root = ET.fromstring(strdata) + + for present in bonus_root.findall("present"): + present_id = present.find("id").text + present_name = present.find("str").text + + item_num = int(bonus_root.find("itemNum").text) + need_login_day_count = int( + bonus_root.find("needLoginDayCount").text + ) + login_bonus_category_type = int( + bonus_root.find("loginBonusCategoryType").text + ) + + result = self.data.static.put_login_bonus( + self.version, + id, + bonus_id, + bonus_name, + present_id, + present_name, + item_num, + need_login_day_count, + login_bonus_category_type, + ) + + if result is not None: + self.logger.info(f"Inserted login bonus {bonus_id}") + else: + self.logger.warn( + f"Failed to insert login bonus {bonus_id}" + ) def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 124d7df..4ffcf93 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -184,8 +184,73 @@ print_detail = Table( mysql_charset="utf8mb4", ) +login_bonus = Table( + "chuni_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("version", Integer, nullable=False), + Column("presetId", Integer, nullable=False), + Column("bonusCount", Integer, nullable=False, server_default="0"), + Column("lastUpdateDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("isWatched", Boolean, server_default="0"), + Column("isFinished", Boolean, server_default="0"), + UniqueConstraint("version", "user", "presetId", name="chuni_item_login_bonus_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def put_login_bonus( + self, user_id: int, version: int, preset_id: int, **login_bonus_data + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, user=user_id, presetId=preset_id, **login_bonus_data + ) + + conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_all_login_bonus( + self, user_id: int, version: int, is_finished: bool = False + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.isFinished == is_finished, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus( + self, user_id: int, version: int, preset_id: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.presetId == preset_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id @@ -335,7 +400,7 @@ class ChuniItemData(BaseData): sql = print_state.select( and_( print_state.c.user == aime_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -351,7 +416,7 @@ class ChuniItemData(BaseData): and_( print_state.c.user == aime_id, print_state.c.gachaId == gacha_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -380,9 +445,7 @@ class ChuniItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: @@ -390,4 +453,4 @@ class ChuniItemData(BaseData): f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" ) return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 4882046..bef58c0 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -122,8 +122,148 @@ gacha_cards = Table( mysql_charset="utf8mb4", ) +login_bonus_preset = Table( + "chuni_static_login_bonus_preset", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presetName", String(255), nullable=False), + Column("isEnabled", Boolean, server_default="1"), + UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + mysql_charset="utf8mb4", +) + +login_bonus = Table( + "chuni_static_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column( + "presetId", + ForeignKey( + "chuni_static_login_bonus_preset.id", + ondelete="cascade", + onupdate="cascade", + ), + nullable=False, + ), + Column("loginBonusId", Integer, nullable=False), + Column("loginBonusName", String(255), nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("itemNum", Integer, nullable=False), + Column("needLoginDayCount", Integer, nullable=False), + Column("loginBonusCategoryType", Integer, nullable=False), + UniqueConstraint( + "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): + def put_login_bonus( + self, + version: int, + preset_id: int, + login_bonus_id: int, + login_bonus_name: str, + present_id: int, + present_ame: str, + item_num: int, + need_login_day_count: int, + login_bonus_category_type: int, + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, + presetId=preset_id, + loginBonusId=login_bonus_id, + loginBonusName=login_bonus_name, + presentId=present_id, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + conflict = sql.on_duplicate_key_update( + loginBonusName=login_bonus_name, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus( + self, version: int, preset_id: int, + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + ) + ).order_by(login_bonus.c.needLoginDayCount.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus_by_required_days( + self, version: int, preset_id: int, need_login_day_count: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + login_bonus.c.needLoginDayCount == need_login_day_count, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_login_bonus_preset( + self, version: int, preset_id: int, preset_name: str, is_enabled: bool + ) -> Optional[int]: + sql = insert(login_bonus_preset).values( + id=preset_id, + version=version, + presetName=preset_name, + isEnabled=is_enabled, + ) + + conflict = sql.on_duplicate_key_update( + presetName=preset_name, isEnabled=is_enabled + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus_presets( + self, version: int, is_enabled: bool = True + ) -> Optional[List[Row]]: + sql = login_bonus_preset.select( + and_( + login_bonus_preset.c.version == version, + login_bonus_preset.c.isEnabled == is_enabled, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: From 571a691e0eebae59a5f46494a697fe58ac4a11f1 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:54:27 +0200 Subject: [PATCH 154/589] chuni: added `use_login_bonus` check to UserLoginBonusApi --- titles/chuni/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 95c35e8..d41d415 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -396,7 +396,8 @@ class ChuniBase: def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) - if user_login_bonus is None: + # ignore the loginBonus request if its disabled in config + if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} user_login_list = [] From a60d52b7422b639ca982e63b07c0e8b5508bef2e Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 30 Mar 2023 22:58:45 +0200 Subject: [PATCH 155/589] chuni: fixed missing login boni IndexError --- titles/chuni/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d41d415..0eaabff 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -69,7 +69,13 @@ class ChuniBase: self.version, preset["id"] ) - # assume its not None + # skip the current bonus preset if no boni were found + if all_login_boni is None or len(all_login_boni) < 1: + self.logger.warn( + f"No bonus entries found for bonus preset {preset['id']}" + ) + continue + max_needed_days = all_login_boni[0]["needLoginDayCount"] # make sure to not show login boni after all days got redeemed From 0f642629a29e52ffa34d7d2bfaba5c14ab6c2907 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 01:53:36 -0400 Subject: [PATCH 156/589] add debug logger for the uri --- index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/index.py b/index.py index 13d826d..be48ac4 100644 --- a/index.py +++ b/index.py @@ -95,6 +95,7 @@ class HttpDispatcher(resource.Resource): ) def render_GET(self, request: Request) -> bytes: + self.logger.debug(request.uri) test = self.map_get.match(request.uri.decode()) client_ip = Utils.get_ip_addr(request) From de5f61b0de76cdd6f312cbebbeac0d691d613d92 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 03:35:14 -0400 Subject: [PATCH 157/589] pokken: add database tables --- titles/pokken/database.py | 6 + titles/pokken/schema/__init__.py | 4 + titles/pokken/schema/item.py | 27 +++++ titles/pokken/schema/match.py | 45 ++++++++ titles/pokken/schema/profile.py | 192 +++++++++++++++++++++++++++++++ titles/pokken/schema/static.py | 12 ++ 6 files changed, 286 insertions(+) create mode 100644 titles/pokken/schema/__init__.py create mode 100644 titles/pokken/schema/item.py create mode 100644 titles/pokken/schema/match.py create mode 100644 titles/pokken/schema/profile.py create mode 100644 titles/pokken/schema/static.py diff --git a/titles/pokken/database.py b/titles/pokken/database.py index f77f172..c940d83 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -1,7 +1,13 @@ from core.data import Data from core.config import CoreConfig +from .schema import * class PokkenData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) + + self.profile = PokkenProfileData(cfg, self.session) + self.match = PokkenMatchData(cfg, self.session) + self.item = PokkenItemData(cfg, self.session) + self.static = PokkenStaticData(cfg, self.session) diff --git a/titles/pokken/schema/__init__.py b/titles/pokken/schema/__init__.py new file mode 100644 index 0000000..81b8132 --- /dev/null +++ b/titles/pokken/schema/__init__.py @@ -0,0 +1,4 @@ +from .profile import PokkenProfileData +from .match import PokkenMatchData +from .item import PokkenItemData +from .static import PokkenStaticData diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py new file mode 100644 index 0000000..686b32f --- /dev/null +++ b/titles/pokken/schema/item.py @@ -0,0 +1,27 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +item = Table( + 'pokken_item', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column('category', Integer), + Column('content', Integer), + Column('type', Integer), + UniqueConstraint('user', 'category', 'content', 'type', name='pokken_item_uk'), + mysql_charset="utf8mb4", +) + +class PokkenItemData(BaseData): + """ + Items obtained as rewards + """ + pass diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py new file mode 100644 index 0000000..afe61f7 --- /dev/null +++ b/titles/pokken/schema/match.py @@ -0,0 +1,45 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +# Pokken sends depressingly little match data... +match_data = Table( + 'pokken_match_data', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('play_mode', Integer), + Column('result', Integer), + Column('ex_ko_num', Integer), + Column('wko_num', Integer), + Column('timeup_win_num', Integer), + Column('cool_ko_num', Integer), + Column('perfect_ko_num', Integer), + Column('use_navi', Integer), + Column('use_navi_cloth', Integer), + Column('use_aid_skill', Integer), + Column('play_date', TIMESTAMP), + mysql_charset="utf8mb4", +) + +class PokkenMatchData(BaseData): + """ + Match logs + """ + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: + pass + + def get_match(self, match_id: int) -> Optional[Row]: + pass + + def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]: + pass + + def get_matches(self, limit: int = 20) -> Optional[List[Row]]: + pass \ No newline at end of file diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py new file mode 100644 index 0000000..ed2471b --- /dev/null +++ b/titles/pokken/schema/profile.py @@ -0,0 +1,192 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me +# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue +profile = Table( + 'pokken_profile', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column('trainer_name', String(16)), # optional + Column('home_region_code', Integer), + Column('home_loc_name', String(255)), + Column('pref_code', Integer), + Column('navi_newbie_flag', Boolean), + Column('navi_enable_flag', Boolean), + Column('pad_vibrate_flag', Boolean), + Column('trainer_rank_point', Integer), + Column('wallet', Integer), + Column('fight_money', Integer), + Column('score_point', Integer), + Column('grade_max_num', Integer), + Column('extra_counter', Integer), # Optional + Column('total_play_days', Integer), + Column('play_date_time', Integer), + Column('lucky_box_fail_num', Integer), + Column('event_reward_get_flag', Integer), + Column('rank_pvp_all', Integer), + Column('rank_pvp_loc', Integer), + Column('rank_cpu_all', Integer), + Column('rank_cpu_loc', Integer), + Column('rank_event', Integer), + Column('awake_num', Integer), + Column('use_support_num', Integer), + Column('rankmatch_flag', Integer), + Column('rankmatch_max', Integer), # Optional + Column('rankmatch_success', Integer), # Optional + Column('beat_num', Integer), # Optional + Column('title_text_id', Integer), + Column('title_plate_id', Integer), + Column('title_decoration_id', Integer), + Column('support_pokemon_list', JSON), # Repeated, Integer + Column('support_set_1', JSON), # Repeated, Integer + Column('support_set_2', JSON), # Repeated, Integer + Column('support_set_3', JSON), # Repeated, Integer + Column('navi_trainer', Integer), + Column('navi_version_id', Integer), + Column('aid_skill_list', JSON), # Repeated, Integer + Column('aid_skill', Integer), + Column('comment_text_id', Integer), + Column('comment_word_id', Integer), + Column('latest_use_pokemon', Integer), + Column('ex_ko_num', Integer), + Column('wko_num', Integer), + Column('timeup_win_num', Integer), + Column('cool_ko_num', Integer), + Column('cool_ko_num', Integer), + Column('perfect_ko_num', Integer), + Column('record_flag', Integer), + Column('continue_num', Integer), + Column('avatar_body', Integer), # Optional + Column('avatar_gender', Integer), # Optional + Column('avatar_background', Integer), # Optional + Column('avatar_head', Integer), # Optional + Column('avatar_battleglass', Integer), # Optional + Column('avatar_face0', Integer), # Optional + Column('avatar_face1', Integer), # Optional + Column('avatar_face2', Integer), # Optional + Column('avatar_bodyall', Integer), # Optional + Column('avatar_wear', Integer), # Optional + Column('avatar_accessory', Integer), # Optional + Column('avatar_stamp', Integer), # Optional + Column('event_state', Integer), + Column('event_id', Integer), + Column('sp_bonus_category_id_1', Integer), + Column('sp_bonus_key_value_1', Integer), + Column('sp_bonus_category_id_2', Integer), + Column('sp_bonus_key_value_2', Integer), + Column('last_play_event_id', Integer), # Optional + mysql_charset="utf8mb4", +) + +tutorial_progress = Table( + 'pokken_tutorial_progress', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_tutorial_progress_uk'), + mysql_charset="utf8mb4", +) + +rankmatch_progress = Table( + 'pokken_rankmatch_progress', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('progress', Integer), + UniqueConstraint('user', 'progress', name='pokken_rankmatch_progress_uk'), + mysql_charset="utf8mb4", +) + +achievement_flag = Table( + 'pokken_achievement_flag', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_achievement_flag_uk'), + mysql_charset="utf8mb4", +) + +event_achievement_flag = Table( + 'pokken_event_achievement_flag', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_event_achievement_flag_uk'), + mysql_charset="utf8mb4", +) + +event_achievement_param = Table( + 'pokken_event_achievement_param', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('param', Integer), + UniqueConstraint('user', 'param', name='pokken_event_achievement_param_uk'), + mysql_charset="utf8mb4", +) + +pokemon_data = Table( + 'pokken_pokemon_data', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('char_id', Integer, nullable=False), + Column('illustration_book_no', Integer, nullable=False), + Column('pokemon_exp', Integer, nullable=False), + Column('battle_num_vs_wan', Integer, nullable=False), + Column('win_vs_wan', Integer, nullable=False), + Column('battle_num_vs_lan', Integer, nullable=False), + Column('win_vs_lan', Integer, nullable=False), + Column('battle_num_vs_cpu', Integer, nullable=False), + Column('win_cpu', Integer, nullable=False), + Column('battle_all_num_tutorial', Integer, nullable=False), + Column('battle_num_tutorial', Integer, nullable=False), + Column('bp_point_atk', Integer, nullable=False), + Column('bp_point_res', Integer, nullable=False), + Column('bp_point_def', Integer, nullable=False), + Column('bp_point_sp', Integer, nullable=False), +) + +class PokkenProfileData(BaseData): + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user = user_id) + conflict = sql.on_duplicate_key_update(user = user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create pokken profile for user {user_id}!") + return None + return result.lastrowid + + def set_profile_name(self, user_id: int, new_name: str) -> None: + sql = update(profile).where(profile.c.user == user_id).values(trainer_name = new_name) + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to update pokken profile name for user {user_id}!") + + def update_profile(self, user_id: int, profile_data: Dict) -> None: + """ + TODO: Find out what comes in on the SaveUserRequestData protobuf and save it! + """ + pass + + def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: + pass + + def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: + pass + + def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: + pass diff --git a/titles/pokken/schema/static.py b/titles/pokken/schema/static.py new file mode 100644 index 0000000..63aa2dd --- /dev/null +++ b/titles/pokken/schema/static.py @@ -0,0 +1,12 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +class PokkenStaticData(BaseData): + pass \ No newline at end of file From 5ec280ab8cb232893f6e3e68a36e273225a6a96d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 04:42:40 -0400 Subject: [PATCH 158/589] pokken: fill LoadUser, add auto_register flag --- example_config/pokken.yaml | 3 +- titles/pokken/base.py | 184 ++++++++++++++++++++++---------- titles/pokken/config.py | 10 ++ titles/pokken/schema/profile.py | 6 ++ 4 files changed, 143 insertions(+), 60 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 7400060..225e980 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -5,4 +5,5 @@ server: port: 9000 port_stun: 9001 port_turn: 9002 - port_admission: 9003 \ No newline at end of file + port_admission: 9003 + auto_register: True \ No newline at end of file diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 1a9d5da..5fa200b 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -5,8 +5,9 @@ import random from core.data import Data from core import CoreConfig -from titles.pokken.config import PokkenConfig -from titles.pokken.proto import jackal_pb2 +from .config import PokkenConfig +from .proto import jackal_pb2 +from .database import PokkenData class PokkenBase: @@ -15,7 +16,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") - self.data = Data(core_cfg) + self.data = PokkenData(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -98,7 +99,7 @@ class PokkenBase: res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS settings = jackal_pb2.LoadClientSettingsResponseData() - settings.money_magnification = 0 + settings.money_magnification = 1 settings.continue_bonus_exp = 100 settings.continue_fight_money = 100 settings.event_bonus_exp = 100 @@ -132,72 +133,130 @@ class PokkenBase: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_USER - access_code = request.load_user.access_code + access_code = request.load_user.access_code + load_usr = jackal_pb2.LoadUserResponseData() user_id = self.data.card.get_user_id_from_card(access_code) - if user_id is None: # TODO: Toggle auto-register + if user_id is None and self.game_cfg.server.auto_register: user_id = self.data.user.create_user() card_id = self.data.card.create_card(user_id, access_code) self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") - # TODO: Check for user data. For now just treat ever card-in as a new user - - load_usr = jackal_pb2.LoadUserResponseData() + elif user_id is None: + self.logger.info(f"Registration of card {access_code} blocked!") + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + """ + TODO: Add repeated values + tutorial_progress_flag + rankmatch_progress + support_pokemon_list + support_set_1 + support_set_2 + support_set_3 + aid_skill_list + achievement_flag + pokemon_data + event_achievement_flag + event_achievement_param + """ + profile = self.data.profile.get_profile(user_id) load_usr.commidserv_result = 1 load_usr.load_hash = 1 load_usr.cardlock_status = False load_usr.banapass_id = user_id - load_usr.access_code = access_code - load_usr.new_card_flag = True + load_usr.access_code = access_code load_usr.precedent_release_flag = 0xFFFFFFFF - load_usr.navi_newbie_flag = True - load_usr.navi_enable_flag = True - load_usr.pad_vibrate_flag = True - load_usr.home_region_code = 0 - load_usr.home_loc_name = "" - load_usr.pref_code = 0 - load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) - load_usr.trainer_rank_point = 0 - load_usr.wallet = 0 - load_usr.fight_money = 0 - load_usr.score_point = 0 - load_usr.grade_max_num = 0 - load_usr.total_play_days = 0 - load_usr.play_date_time = 0 - load_usr.lucky_box_fail_num = 0 - load_usr.event_reward_get_flag = 0 - load_usr.rank_pvp_all = 0 - load_usr.rank_pvp_loc = 0 - load_usr.rank_cpu_all = 0 - load_usr.rank_cpu_loc = 0 - load_usr.rank_event = 0 - load_usr.awake_num = 0 - load_usr.use_support_num = 0 - load_usr.rankmatch_flag = 0 - load_usr.title_text_id = 0 - load_usr.title_plate_id = 0 - load_usr.title_decoration_id = 0 - load_usr.navi_trainer = 0 - load_usr.navi_version_id = 0 - load_usr.aid_skill = 0 - load_usr.comment_text_id = 0 - load_usr.comment_word_id = 0 - load_usr.latest_use_pokemon = 0 - load_usr.ex_ko_num = 0 - load_usr.wko_num = 0 - load_usr.timeup_win_num = 0 - load_usr.cool_ko_num = 0 - load_usr.perfect_ko_num = 0 - load_usr.record_flag = 0 - load_usr.site_register_status = 0 - load_usr.continue_num = 0 - load_usr.event_state = 0 - load_usr.event_id = 0 - load_usr.sp_bonus_category_id_1 = 0 - load_usr.sp_bonus_key_value_1 = 0 - load_usr.sp_bonus_category_id_2 = 0 - load_usr.sp_bonus_key_value_2 = 0 + + if profile is None: + profile_id = self.data.profile.create_profile(user_id) + profile_dict = {'id': profile_id, 'user': user_id} + pokemon_data = [] + tutorial_progress = [] + rankmatch_progress = [] + achievement_flag = [] + event_achievement_flag = [] + event_achievement_param = [] + load_usr.new_card_flag = True + + else: + profile_dict = profile._asdict() + pokemon_data = self.data.profile.get_all_pokemon_data(user_id) + tutorial_progress = [] + rankmatch_progress = [] + achievement_flag = [] + event_achievement_flag = [] + event_achievement_param = [] + load_usr.new_card_flag = False + + load_usr.navi_newbie_flag = profile_dict.get('navi_newbie_flag', True) + load_usr.navi_enable_flag = profile_dict.get('navi_enable_flag', True) + load_usr.pad_vibrate_flag = profile_dict.get('pad_vibrate_flag', True) + load_usr.home_region_code = profile_dict.get('home_region_code', 0) + load_usr.home_loc_name = profile_dict.get('home_loc_name', "") + load_usr.pref_code = profile_dict.get('pref_code', 0) + load_usr.trainer_name = profile_dict.get('trainer_name', "Newb" + str(random.randint(1111,999999))) + load_usr.trainer_rank_point = profile_dict.get('trainer_rank_point', 0) + load_usr.wallet = profile_dict.get('wallet', 0) + load_usr.fight_money = profile_dict.get('fight_money', 0) + load_usr.score_point = profile_dict.get('score_point', 0) + load_usr.grade_max_num = profile_dict.get('grade_max_num', 0) + load_usr.extra_counter = profile_dict.get('extra_counter', 0) + load_usr.total_play_days = profile_dict.get('total_play_days', 0) + load_usr.play_date_time = profile_dict.get('play_date_time', 0) + load_usr.lucky_box_fail_num = profile_dict.get('lucky_box_fail_num', 0) + load_usr.event_reward_get_flag = profile_dict.get('event_reward_get_flag', 0) + load_usr.rank_pvp_all = profile_dict.get('rank_pvp_all', 0) + load_usr.rank_pvp_loc = profile_dict.get('rank_pvp_loc', 0) + load_usr.rank_cpu_all = profile_dict.get('rank_cpu_all', 0) + load_usr.rank_cpu_loc = profile_dict.get('rank_cpu_loc', 0) + load_usr.rank_event = profile_dict.get('rank_event', 0) + load_usr.awake_num = profile_dict.get('awake_num', 0) + load_usr.use_support_num = profile_dict.get('use_support_num', 0) + load_usr.rankmatch_flag = profile_dict.get('rankmatch_flag', 0) + load_usr.rankmatch_max = profile_dict.get('rankmatch_max', 0) + load_usr.rankmatch_success = profile_dict.get('rankmatch_success', 0) + load_usr.beat_num = profile_dict.get('beat_num', 0) + load_usr.title_text_id = profile_dict.get('title_text_id', 0) + load_usr.title_plate_id = profile_dict.get('title_plate_id', 0) + load_usr.title_decoration_id = profile_dict.get('title_decoration_id', 0) + load_usr.navi_trainer = profile_dict.get('navi_trainer', 0) + load_usr.navi_version_id = profile_dict.get('navi_version_id', 0) + load_usr.aid_skill = profile_dict.get('aid_skill', 0) + load_usr.comment_text_id = profile_dict.get('comment_text_id', 0) + load_usr.comment_word_id = profile_dict.get('comment_word_id', 0) + load_usr.latest_use_pokemon = profile_dict.get('latest_use_pokemon', 0) + load_usr.ex_ko_num = profile_dict.get('ex_ko_num', 0) + load_usr.wko_num = profile_dict.get('wko_num', 0) + load_usr.timeup_win_num = profile_dict.get('timeup_win_num', 0) + load_usr.cool_ko_num = profile_dict.get('cool_ko_num', 0) + load_usr.perfect_ko_num = profile_dict.get('perfect_ko_num', 0) + load_usr.record_flag = profile_dict.get('record_flag', 0) + load_usr.site_register_status = profile_dict.get('site_register_status', 0) + load_usr.continue_num = profile_dict.get('continue_num', 0) + + load_usr.avatar_body = profile_dict.get('avatar_body', 0) + load_usr.avatar_gender = profile_dict.get('avatar_gender', 0) + load_usr.avatar_background = profile_dict.get('avatar_background', 0) + load_usr.avatar_head = profile_dict.get('avatar_head', 0) + load_usr.avatar_battleglass = profile_dict.get('avatar_battleglass', 0) + load_usr.avatar_face0 = profile_dict.get('avatar_face0', 0) + load_usr.avatar_face1 = profile_dict.get('avatar_face1', 0) + load_usr.avatar_face2 = profile_dict.get('avatar_face2', 0) + load_usr.avatar_bodyall = profile_dict.get('avatar_bodyall', 0) + load_usr.avatar_wear = profile_dict.get('avatar_wear', 0) + load_usr.avatar_accessory = profile_dict.get('avatar_accessory', 0) + load_usr.avatar_stamp = profile_dict.get('avatar_stamp', 0) + + load_usr.event_state = profile_dict.get('event_state', 0) + load_usr.event_id = profile_dict.get('event_id', 0) + load_usr.sp_bonus_category_id_1 = profile_dict.get('sp_bonus_category_id_1', 0) + load_usr.sp_bonus_key_value_1 = profile_dict.get('sp_bonus_key_value_1', 0) + load_usr.sp_bonus_category_id_2 = profile_dict.get('sp_bonus_category_id_2', 0) + load_usr.sp_bonus_key_value_2 = profile_dict.get('sp_bonus_key_value_2', 0) + load_usr.last_play_event_id = profile_dict.get('last_play_event_id', 0) res.load_user.CopyFrom(load_usr) return res.SerializeToString() @@ -208,10 +267,17 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK return res.SerializeToString() + def handle_save_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_USER + + return res.SerializeToString() + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 - res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG return res.SerializeToString() def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: diff --git a/titles/pokken/config.py b/titles/pokken/config.py index b53fc86..e25a8c7 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -49,6 +49,16 @@ class PokkenServerConfig: self.__config, "pokken", "server", "port_admission", default=9003 ) + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with pokken + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "auto_register", default=True + ) + class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index ed2471b..da6736c 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -182,6 +182,12 @@ class PokkenProfileData(BaseData): """ pass + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: return None + return result.fetchone() + def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: pass From bf6c7d39f5255337ce7fdb4931874525a3d39637 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 04:47:19 -0400 Subject: [PATCH 159/589] pokken: small cleanup on LoadUser --- titles/pokken/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 5fa200b..96d5efd 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -182,7 +182,8 @@ class PokkenBase: load_usr.new_card_flag = True else: - profile_dict = profile._asdict() + profile_dict = { k: v for k, v in profile._asdict().items() if v is not None } + self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})") pokemon_data = self.data.profile.get_all_pokemon_data(user_id) tutorial_progress = [] rankmatch_progress = [] From f63dd079370aca81378fd221cd545ae7fc67846b Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 10 Apr 2023 18:58:19 +0200 Subject: [PATCH 160/589] maimai: Initial Festival support --- core/data/schema/versions/SDEZ_3_rollback.sql | 31 ++ core/data/schema/versions/SDEZ_4_upgrade.sql | 31 ++ docs/game_specific_info.md | 3 +- readme.md | 4 +- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 278 ++++++++++-------- titles/mai2/const.py | 14 +- titles/mai2/festival.py | 31 ++ titles/mai2/index.py | 45 +-- titles/mai2/schema/item.py | 72 +++-- titles/mai2/schema/profile.py | 22 +- titles/mai2/schema/score.py | 2 + titles/mai2/schema/static.py | 5 +- titles/mai2/universeplus.py | 5 +- 14 files changed, 347 insertions(+), 198 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_3_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_4_upgrade.sql create mode 100644 titles/mai2/festival.py diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql new file mode 100644 index 0000000..f819855 --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +DROP COLUMN tapSe; + +ALTER TABLE mai2_score_best +DROP COLUMN extNum1; + +ALTER TABLE mai2_profile_extend +DROP COLUMN playStatusSetting; + +ALTER TABLE mai2_playlog +DROP COLUMN extNum4; + +ALTER TABLE mai2_static_event +DROP COLUMN startDate; + +ALTER TABLE mai2_item_map +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN isLock is_lock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, +CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, +CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonusId bonus_id INT NOT NULL, +CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql new file mode 100644 index 0000000..52d120a --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +ADD COLUMN tapSe INT NOT NULL DEFAULT 0 AFTER tapDesign; + +ALTER TABLE mai2_score_best +ADD COLUMN extNum1 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_profile_extend +ADD COLUMN playStatusSetting INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_playlog +ADD COLUMN extNum4 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_static_event +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE mai2_item_map +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN is_lock isLock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_clear isClear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN season_id seasonId INT NOT NULL, +CHANGE COLUMN reward_get rewardGet BOOLEAN NOT NULL, +CHANGE COLUMN user_name userName VARCHAR(8) NOT NULL, +CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonus_id bonusId INT NOT NULL, +CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index b09d61e..1f7872c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -114,6 +114,7 @@ Config file is located in `config/cxb.yaml`. | 3 | maimai DX Splash PLUS | | 4 | maimai DX Universe | | 5 | maimai DX Universe PLUS | +| 6 | maimai DX Festival | ### Importer @@ -126,7 +127,7 @@ python read.py --series SDEZ --version --binfolder /path/to/game/fo 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!** +crash without Events!** ### Database upgrade diff --git a/readme.md b/readme.md index 4afc225..02f0c67 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + Crossbeats Rev + All versions + omnimix -+ Maimai - + All versions up to Universe Plus ++ maimai DX + + All versions up to Festival + Hatsune Miku Arcade + All versions diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 27fba3a..810eac9 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 = 3 +current_schema_version = 4 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 741ccb6..2e7198d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,5 +1,5 @@ from datetime import datetime, date, timedelta -from typing import Dict +from typing import Any, Dict import logging from core.config import CoreConfig @@ -52,6 +52,7 @@ class Mai2Base: events = self.data.static.get_enabled_events(self.version) events_lst = [] if events is None: + self.logger.warn("No enabled events, did you run the reader?") return {"type": data["type"], "length": 0, "gameEventList": []} for event in events: @@ -59,7 +60,11 @@ class Mai2Base: { "type": event["type"], "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + event["startDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), "endDate": "2099-12-31 00:00:00.0", } ) @@ -79,12 +84,12 @@ class Mai2Base: return {"length": 0, "gameChargeList": []} charge_list = [] - for x in range(len(game_charge_list)): + for i, charge in enumerate(game_charge_list): charge_list.append( { - "orderId": x, - "chargeId": game_charge_list[x]["ticketId"], - "price": game_charge_list[x]["price"], + "orderId": i, + "chargeId": charge["ticketId"], + "price": charge["price"], "startDate": "2017-12-05 07:00:00.0", "endDate": "2099-12-31 00:00:00.0", } @@ -167,6 +172,20 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -204,15 +223,21 @@ class Mai2Base: if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], - datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"), - datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S") + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), ) - if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: self.data.item.put_character( user_id, @@ -222,7 +247,7 @@ class Mai2Base: char["useCount"], ) - if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0: + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: self.data.item.put_item( user_id, @@ -232,7 +257,7 @@ class Mai2Base: item["isValid"], ) - if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0: + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: self.data.item.put_login_bonus( user_id, @@ -242,7 +267,7 @@ class Mai2Base: login_bonus["isComplete"], ) - if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0: + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: self.data.item.put_map( user_id, @@ -253,21 +278,27 @@ class Mai2Base: map["isComplete"], ) - if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0: + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, music) - if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0: + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: self.data.score.put_course(user_id, course) - if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0: + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - # if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: - # for fsr in upsert["userFriendSeasonRankingList"]: - # pass + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime(fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) def handle_user_logout_api_request(self, data: Dict) -> Dict: pass @@ -311,11 +342,7 @@ class Mai2Base: def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: - return { - "userId": data["userId"], - "nextIndex": 0, - "userCardList": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -333,25 +360,23 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["startDate"] = datetime.strftime( - tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["endDate"] = datetime.strftime( - tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) card_list.append(tmp) return { "userId": data["userId"], "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx] + "userCardList": card_list[start_idx:end_idx], } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charges = self.data.item.get_charges(data["userId"]) if user_charges is None: - return { - "userId": data["userId"], - "length": 0, - "userChargeList": [] - } + return {"userId": data["userId"], "length": 0, "userChargeList": []} user_charge_list = [] for charge in user_charges: @@ -359,45 +384,46 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["purchaseDate"] = datetime.strftime( - tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S") + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["validDate"] = datetime.strftime( - tmp["validDate"], "%Y-%m-%d %H:%M:%S") + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) user_charge_list.append(tmp) return { "userId": data["userId"], "length": len(user_charge_list), - "userChargeList": user_charge_list + "userChargeList": user_charge_list, } def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_items = self.data.item.get_items(data["userId"], kind) - user_item_list = [] - next_idx = 0 + user_item_list = self.data.item.get_items(data["userId"], kind) - for x in range(next_idx, data["maxCount"]): - try: - user_item_list.append({ - "itemKind": user_items[x]["itemKind"], - "itemId": user_items[x]["itemId"], - "stock": user_items[x]["stock"], - "isValid": user_items[x]["isValid"] - }) - except IndexError: + items: list[Dict[str, Any]] = [] + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): break - if len(user_item_list) == data["maxCount"]: - next_idx = data["nextIndex"] + data["maxCount"] + 1 - break + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout return { "userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, - "userItemList": user_item_list + "userItemList": items, } def handle_get_user_character_api_request(self, data: Dict) -> Dict: @@ -479,21 +505,12 @@ 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": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} course_list = [] for course in user_courses: @@ -502,11 +519,7 @@ 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 @@ -514,96 +527,103 @@ class Mai2Base: def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) - friend_season_ranking_list = [] - next_index = 0 + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - friend_season_ranking_list.append( - { - "mapId": friend_season_ranking_list[x]["map_id"], - "distance": friend_season_ranking_list[x]["distance"], - "isLock": friend_season_ranking_list[x]["is_lock"], - "isClear": friend_season_ranking_list[x]["is_clear"], - "isComplete": friend_season_ranking_list[x]["is_complete"], - } - ) - except: + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(friend_season_ranking_list) == data["maxCount"] - and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userFriendSeasonRankingList": friend_season_ranking_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) - map_list = [] - next_index = 0 + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - map_list.append( - { - "mapId": maps[x]["map_id"], - "distance": maps[x]["distance"], - "isLock": maps[x]["is_lock"], - "isClear": maps[x]["is_clear"], - "isComplete": maps[x]["is_complete"], - } - ) - except: + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(map_list) == data["maxCount"] - and len(maps) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userMapList": map_list, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) - login_bonus_list = [] - next_index = 0 + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - login_bonus_list.append( - { - "bonusId": login_bonuses[x]["bonus_id"], - "point": login_bonuses[x]["point"], - "isCurrent": login_bonuses[x]["is_current"], - "isComplete": login_bonuses[x]["is_complete"], - } - ) - except: + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(login_bonus_list) == data["maxCount"] - and len(login_bonuses) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userLoginBonusList": login_bonus_list, } @@ -629,5 +649,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/const.py b/titles/mai2/const.py index dd1dca0..9694aba 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -30,14 +30,16 @@ class Mai2Constants: VER_MAIMAI_DX_SPLASH_PLUS = 3 VER_MAIMAI_DX_UNIVERSE = 4 VER_MAIMAI_DX_UNIVERSE_PLUS = 5 + VER_MAIMAI_DX_FESTIVAL = 6 VERSION_STRING = ( - "maimai Delux", - "maimai Delux PLUS", - "maimai Delux Splash", - "maimai Delux Splash PLUS", - "maimai Delux Universe", - "maimai Delux Universe PLUS", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX Universe", + "maimai DX Universe PLUS", + "maimai DX Festival" ) @classmethod diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py new file mode 100644 index 0000000..4e51619 --- /dev/null +++ b/titles/mai2/festival.py @@ -0,0 +1,31 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Festival(Mai2UniversePlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.36 + user_data["lastDataVersion"] = "1.30.00" + return user_data + + def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = super().handle_user_login_api_request(data) + # useless? + user_login["Bearer"] = "ARTEMiSTOKEN" + return user_login + + def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 3cd1629..3618f4d 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -10,6 +10,7 @@ from os import path from typing import Tuple from core.config import CoreConfig +from core.utils import Utils from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants from titles.mai2.base import Mai2Base @@ -18,6 +19,7 @@ from titles.mai2.splash import Mai2Splash from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.universe import Mai2Universe from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.festival import Mai2Festival class Mai2Servlet: @@ -30,12 +32,13 @@ class Mai2Servlet: ) self.versions = [ - Mai2Base(core_cfg, self.game_cfg), - Mai2Plus(core_cfg, self.game_cfg), - Mai2Splash(core_cfg, self.game_cfg), - Mai2SplashPlus(core_cfg, self.game_cfg), - Mai2Universe(core_cfg, self.game_cfg), - Mai2UniversePlus(core_cfg, self.game_cfg), + Mai2Base, + Mai2Plus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival ] self.logger = logging.getLogger("mai2") @@ -97,6 +100,7 @@ class Mai2Servlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX @@ -108,8 +112,10 @@ class Mai2Servlet: internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS elif version >= 120 and version < 125: # Universe internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125: # Universe Plus + elif version >= 125 and version < 130: # Universe Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130: # Festival + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL 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 @@ -128,25 +134,30 @@ class Mai2Servlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + 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}") - return zlib.compress(b'{"returnCode": 1}') + resp = {"returnCode": 1} - try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + else: + try: + handler = getattr(handler_cls, func_to_find) + resp = handler(req_data) - except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {"returnCode": 1} - self.logger.info(f"Response {resp}") + self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d64d954..c6b2e7a 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -71,12 +71,12 @@ map = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("map_id", Integer, nullable=False), + Column("mapId", 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"), + Column("isLock", Boolean, nullable=False, server_default="0"), + Column("isClear", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), mysql_charset="utf8mb4", ) @@ -89,11 +89,11 @@ login_bonus = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("bonus_id", Integer, nullable=False), + Column("bonusId", 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"), + Column("isCurrent", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), mysql_charset="utf8mb4", ) @@ -106,13 +106,15 @@ friend_season_ranking = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("season_id", Integer, nullable=False), + Column("seasonId", Integer, nullable=False), Column("point", Integer, nullable=False), Column("rank", Integer, nullable=False), - Column("reward_get", Boolean, nullable=False), - Column("user_name", String(8), nullable=False), - Column("record_date", String(255), nullable=False), - UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"), + Column("rewardGet", Boolean, nullable=False), + Column("userName", String(8), nullable=False), + Column("recordDate", TIMESTAMP, nullable=False), + UniqueConstraint( + "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" + ), mysql_charset="utf8mb4", ) @@ -293,18 +295,18 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(map).values( user=user_id, - map_id=map_id, + mapId=map_id, distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) result = self.execute(conflict) @@ -324,7 +326,7 @@ class Mai2ItemData(BaseData): return result.fetchall() def get_map(self, user_id: int, map_id: int) -> Optional[Row]: - sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id)) + sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) result = self.execute(sql) if result is None: @@ -341,16 +343,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(character).values( user=user_id, - character_id=character_id, + characterId=character_id, level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) conflict = sql.on_duplicate_key_update( level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) result = self.execute(conflict) @@ -385,7 +387,25 @@ class Mai2ItemData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() + + def put_friend_season_ranking( + self, aime_id: int, friend_season_ranking_data: Dict + ) -> Optional[int]: + sql = insert(friend_season_ranking).values( + user=aime_id, **friend_season_ranking_data + ) + + conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_friend_season_ranking: failed to insert", + f"friend_season_ranking! aime_id: {aime_id}" + ) + return None + return result.lastrowid def put_favorite( self, user_id: int, kind: int, item_id_list: List[int] diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 1ce8046..b3802e5 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -158,6 +158,7 @@ extend = Table( Column("sortMusicSetting", Integer), Column("selectedCardList", JSON), Column("encountMapNpcList", JSON), + Column("playStatusSetting", Integer, server_default="0"), UniqueConstraint("user", "version", name="mai2_profile_extend_uk"), mysql_charset="utf8mb4", ) @@ -178,6 +179,7 @@ option = Table( Column("slideSpeed", Integer), Column("touchSpeed", Integer), Column("tapDesign", Integer), + Column("tapSe", Integer, server_default="0"), Column("holdDesign", Integer), Column("slideDesign", Integer), Column("starType", Integer), @@ -298,8 +300,8 @@ class Mai2ProfileData(BaseData): 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) - ) + and_(detail.c.user == user_id, detail.c.version <= version) + ).order_by(detail.c.version.desc()) result = self.execute(sql) if result is None: @@ -323,8 +325,8 @@ class Mai2ProfileData(BaseData): 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) - ) + and_(ghost.c.user == user_id, ghost.c.version_int <= version) + ).order_by(ghost.c.version.desc()) result = self.execute(sql) if result is None: @@ -348,8 +350,8 @@ class Mai2ProfileData(BaseData): 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) - ) + and_(extend.c.user == user_id, extend.c.version <= version) + ).order_by(extend.c.version.desc()) result = self.execute(sql) if result is None: @@ -373,8 +375,8 @@ class Mai2ProfileData(BaseData): 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) - ) + and_(option.c.user == user_id, option.c.version <= version) + ).order_by(option.c.version.desc()) result = self.execute(sql) if result is None: @@ -398,8 +400,8 @@ class Mai2ProfileData(BaseData): 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) - ) + and_(rating.c.user == user_id, rating.c.version <= version) + ).order_by(rating.c.version.desc()) result = self.execute(sql) if result is None: diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 4d3291d..0f7f239 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -25,6 +25,7 @@ best_score = Table( Column("syncStatus", Integer), Column("deluxscoreMax", Integer), Column("scoreRank", Integer), + Column("extNum1", Integer, server_default="0"), UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"), mysql_charset="utf8mb4", ) @@ -143,6 +144,7 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), + Column("extNum4", Integer, server_default="0"), Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index e40e37f..b22dcc4 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -16,6 +16,7 @@ event = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), mysql_charset="utf8mb4", @@ -108,7 +109,7 @@ class Mai2StaticData(BaseData): return None return result.fetchall() - def toggle_game_events( + def toggle_game_event( self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( @@ -118,7 +119,7 @@ class Mai2StaticData(BaseData): 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}" + f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}" ) return result.last_updated_params() diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 54fe896..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,7 +1,4 @@ -from typing import Any, List, Dict -from datetime import datetime, timedelta -import pytz -import json +from typing import Dict from core.config import CoreConfig from titles.mai2.universe import Mai2Universe From 68e25b9c5eebcefb87f70c8ee6cbcec67596584a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 11 Apr 2023 11:40:05 -0400 Subject: [PATCH 161/589] pokken: add frontend stub --- core/frontend.py | 7 ++--- titles/pokken/__init__.py | 8 +++--- titles/pokken/frontend.py | 33 +++++++++++++++++++++++ titles/pokken/frontend/pokken_index.jinja | 4 +++ 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 titles/pokken/frontend.py create mode 100644 titles/pokken/frontend/pokken_index.jinja diff --git a/core/frontend.py b/core/frontend.py index 127b174..0c96fdc 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -66,13 +66,14 @@ class FrontendServlet(resource.Resource): fe_game = FE_Game(cfg, self.environment) games = Utils.get_all_titles() for game_dir, game_mod in games.items(): - if hasattr(game_mod, "frontend"): + if hasattr(game_mod, "frontend"): try: game_fe = game_mod.frontend(cfg, self.environment, config_dir) self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) fe_game.putChild(game_dir.encode(), game_fe) - except: - raise + + except Exception as e: + self.logger.error(f"Failed to import frontend from {game_dir} because {e}") self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index ed2ee23..94237c4 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -1,8 +1,10 @@ -from titles.pokken.index import PokkenServlet -from titles.pokken.const import PokkenConstants -from titles.pokken.database import PokkenData +from .index import PokkenServlet +from .const import PokkenConstants +from .database import PokkenData +from .frontend import PokkenFrontend index = PokkenServlet database = PokkenData game_codes = [PokkenConstants.GAME_CODE] current_schema_version = 1 +frontend = PokkenFrontend diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py new file mode 100644 index 0000000..e4e8947 --- /dev/null +++ b/titles/pokken/frontend.py @@ -0,0 +1,33 @@ +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 +from .database import PokkenData +from .config import PokkenConfig +from .const import PokkenConstants + + +class PokkenFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = PokkenData(cfg) + self.game_cfg = PokkenConfig() + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) + self.nav_name = "Pokken" + + def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/pokken/frontend/pokken_index.jinja" + ) + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + ).encode("utf-16") diff --git a/titles/pokken/frontend/pokken_index.jinja b/titles/pokken/frontend/pokken_index.jinja new file mode 100644 index 0000000..446893a --- /dev/null +++ b/titles/pokken/frontend/pokken_index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Pokken

+{% endblock content %} \ No newline at end of file From 28c06335b648b9f02a374feca7b471bc3170712c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 11 Apr 2023 17:57:21 +0200 Subject: [PATCH 162/589] mai2: added upsert returns, fixed event reader, thanks @One3 Thanks to @One3 for helping with the events --- titles/mai2/base.py | 16 +++++++--- titles/mai2/read.py | 61 +++++++++++++++++++++++++++++++++++- titles/mai2/schema/item.py | 10 +++--- titles/mai2/schema/static.py | 2 +- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2e7198d..9f5c8af 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -98,16 +98,16 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) @@ -172,6 +172,8 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -186,6 +188,8 @@ class Mai2Base: datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), ) + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -300,8 +304,10 @@ class Mai2Base: ) self.data.item.put_friend_season_ranking(user_id, fsr) + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + def handle_user_logout_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 2c0567c..5809464 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -29,7 +29,7 @@ class Mai2Reader(BaseReader): f"Start importer for {Mai2Constants.game_ver_to_string(version)}" ) except IndexError: - self.logger.error(f"Invalid maidx version {version}") + self.logger.error(f"Invalid maimai DX version {version}") exit(1) def read(self) -> None: @@ -43,6 +43,7 @@ class Mai2Reader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") self.read_music(f"{dir}/music") self.read_tickets(f"{dir}/ticket") @@ -64,6 +65,64 @@ class Mai2Reader(BaseReader): ) self.logger.info(f"Added event {id}...") + def disable_events( + self, base_information_dir: str, base_score_ranking_dir: str + ) -> None: + self.logger.info(f"Reading disabled events from {base_information_dir}...") + + for root, dirs, files in os.walk(base_information_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Information.xml"): + with open(f"{root}/{dir}/Information.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("name").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + for root, dirs, files in os.walk(base_score_ranking_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/ScoreRanking.xml"): + with open(f"{root}/{dir}/ScoreRanking.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("eventName").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + # manually disable events wich are known to be problematic + for event_id in [ + 1, + 10, + 220311, + 220312, + 220313, + 220314, + 220315, + 220316, + 220317, + 220318, + 20121821, + 21121651, + 22091511, + 22091512, + 22091513, + 22091514, + 22091515, + 22091516, + 22091517, + 22091518, + 22091519, + ]: + self.data.static.toggle_game_event(self.version, event_id, toggle=False) + self.logger.info(f"Disabled event {event_id}...") + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index c6b2e7a..2dc5f5e 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -246,16 +246,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(login_bonus).values( user=user_id, - bonus_id=bonus_id, + bonusId=bonus_id, point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) result = self.execute(conflict) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index b22dcc4..76b163c 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -113,7 +113,7 @@ class Mai2StaticData(BaseData): self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( - and_(event.c.version == version, event.c.event_id == event_id) + and_(event.c.version == version, event.c.eventId == event_id) ).values(enabled=int(toggle)) result = self.execute(sql) From bd356af2726dfdc05f0278a23d866ba7d7b4b6c0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 12 Apr 2023 02:34:29 -0400 Subject: [PATCH 163/589] pokken: restructure database --- .gitignore | 1 + core/allnet.py | 2 +- titles/pokken/base.py | 7 ++++ titles/pokken/index.py | 5 ++- titles/pokken/schema/match.py | 5 ++- titles/pokken/schema/profile.py | 69 ++++++--------------------------- 6 files changed, 27 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 825729f..b5a0e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,5 +158,6 @@ cert/* !cert/server.pem config/* deliver/* +*.gz dbdump-*.json \ No newline at end of file diff --git a/core/allnet.py b/core/allnet.py index 0d4fbf7..b7cc4c6 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -55,7 +55,7 @@ class AllnetServlet: self.logger.error("No games detected!") for _, mod in plugins.items(): - if hasattr(mod.index, "get_allnet_info"): + if hasattr(mod, "index") and 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 diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 96d5efd..0e87397 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -17,6 +17,7 @@ class PokkenBase: self.version = 0 self.logger = logging.getLogger("pokken") self.data = PokkenData(core_cfg) + self.SUPPORT_SET_NONE = 4294967295 def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -281,6 +282,12 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG return res.SerializeToString() + def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_CHARGE + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index a7a328f..adfde88 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -87,7 +87,7 @@ class PokkenServlet(resource.Resource): if not game_cfg.server.enable: return (False, "") - return (True, "PKF2") + return (True, "PKF1") def setup(self) -> None: # TODO: Setup stun, turn (UDP) and admission (WSS) servers @@ -114,6 +114,8 @@ class PokkenServlet(resource.Resource): endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ pokken_request.type ].name.lower() + + self.logger.debug(pokken_request) handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: @@ -121,7 +123,6 @@ class PokkenServlet(resource.Resource): 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) return ret diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py index afe61f7..aec6bd3 100644 --- a/titles/pokken/schema/match.py +++ b/titles/pokken/schema/match.py @@ -14,8 +14,9 @@ match_data = Table( metadata, Column('id', Integer, primary_key=True, nullable=False), Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('play_mode', Integer), - Column('result', Integer), + Column('num_games', Integer), + Column('play_modes', JSON), + Column('results', JSON), Column('ex_ko_num', Integer), Column('wko_num', Integer), Column('timeup_win_num', Integer), diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index da6736c..bab53de 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -28,8 +28,10 @@ profile = Table( Column('score_point', Integer), Column('grade_max_num', Integer), Column('extra_counter', Integer), # Optional + Column('tutorial_progress_flag', JSON), # Repeated, Integer Column('total_play_days', Integer), Column('play_date_time', Integer), + Column('achievement_flag', JSON), # Repeated, Integer Column('lucky_box_fail_num', Integer), Column('event_reward_get_flag', Integer), Column('rank_pvp_all', Integer), @@ -41,15 +43,19 @@ profile = Table( Column('use_support_num', Integer), Column('rankmatch_flag', Integer), Column('rankmatch_max', Integer), # Optional + Column('rankmatch_progress', JSON), # Repeated, Integer Column('rankmatch_success', Integer), # Optional Column('beat_num', Integer), # Optional Column('title_text_id', Integer), Column('title_plate_id', Integer), Column('title_decoration_id', Integer), Column('support_pokemon_list', JSON), # Repeated, Integer - Column('support_set_1', JSON), # Repeated, Integer - Column('support_set_2', JSON), # Repeated, Integer - Column('support_set_3', JSON), # Repeated, Integer + Column('support_set_1_1', Integer), # Repeated, Integer + Column('support_set_1_2', Integer), + Column('support_set_2_1', Integer), # Repeated, Integer + Column('support_set_2_2', Integer), + Column('support_set_3_1', Integer), # Repeated, Integer + Column('support_set_3_2', Integer), Column('navi_trainer', Integer), Column('navi_version_id', Integer), Column('aid_skill_list', JSON), # Repeated, Integer @@ -84,56 +90,8 @@ profile = Table( Column('sp_bonus_category_id_2', Integer), Column('sp_bonus_key_value_2', Integer), Column('last_play_event_id', Integer), # Optional - mysql_charset="utf8mb4", -) - -tutorial_progress = Table( - 'pokken_tutorial_progress', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_tutorial_progress_uk'), - mysql_charset="utf8mb4", -) - -rankmatch_progress = Table( - 'pokken_rankmatch_progress', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('progress', Integer), - UniqueConstraint('user', 'progress', name='pokken_rankmatch_progress_uk'), - mysql_charset="utf8mb4", -) - -achievement_flag = Table( - 'pokken_achievement_flag', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_achievement_flag_uk'), - mysql_charset="utf8mb4", -) - -event_achievement_flag = Table( - 'pokken_event_achievement_flag', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_event_achievement_flag_uk'), - mysql_charset="utf8mb4", -) - -event_achievement_param = Table( - 'pokken_event_achievement_param', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('param', Integer), - UniqueConstraint('user', 'param', name='pokken_event_achievement_param_uk'), + Column('event_achievement_flag', JSON), # Repeated, Integer + Column('event_achievement_param', JSON), # Repeated, Integer mysql_charset="utf8mb4", ) @@ -176,10 +134,7 @@ class PokkenProfileData(BaseData): if result is None: self.logger.error(f"Failed to update pokken profile name for user {user_id}!") - def update_profile(self, user_id: int, profile_data: Dict) -> None: - """ - TODO: Find out what comes in on the SaveUserRequestData protobuf and save it! - """ + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass def get_profile(self, user_id: int) -> Optional[Row]: From 71c43a4a57475b236b52b11afef7e50a37285e2c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 12 Apr 2023 02:39:56 -0400 Subject: [PATCH 164/589] pokken: add_profile_points stub --- titles/pokken/schema/profile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index bab53de..37729fc 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -137,6 +137,9 @@ class PokkenProfileData(BaseData): def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass + def add_profile_points(self, user_id: int, rank_pts: int, money: int, score_pts: int) -> None: + pass + def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = self.execute(sql) From 97e3f1af0160a13b652d872b546018cfe3221460 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 13 Apr 2023 22:22:28 +0200 Subject: [PATCH 165/589] mai2: cardmaker festival support --- titles/cm/index.py | 7 ++++++- titles/cm/read.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/titles/cm/index.py b/titles/cm/index.py index d544e59..3a56566 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -12,6 +12,7 @@ from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig +from core.utils import Utils from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase @@ -82,6 +83,7 @@ class CardMakerServlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) print(f"version: {version}") @@ -107,7 +109,10 @@ class CardMakerServlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/cm/read.py b/titles/cm/read.py index f27b40b..93ff203 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -80,7 +80,7 @@ class CardMakerReader(BaseReader): for dir in data_dirs: self.read_chuni_card(f"{dir}/CHU/card") self.read_chuni_gacha(f"{dir}/CHU/gacha") - + self.read_mai2_card(f"{dir}/MAI/card") self.read_ongeki_gacha(f"{dir}/MU3/gacha") def read_chuni_card(self, base_dir: str) -> None: @@ -206,6 +206,7 @@ class CardMakerReader(BaseReader): "1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS, "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, + "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, } for root, dirs, files in os.walk(base_dir): From dc3e3e1fb34be7caed009ee6df7dd4ad570fdf4e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 14 Apr 2023 02:51:28 -0400 Subject: [PATCH 166/589] pokken: add constants, add stats to profile table --- titles/pokken/const.py | 12 +++++++ titles/pokken/schema/profile.py | 55 +++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 802a7b9..81b2ebc 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,3 +1,5 @@ +from enum import Enum + class PokkenConstants: GAME_CODE = "SDAK" @@ -7,6 +9,16 @@ class PokkenConstants: VERSION_NAMES = "Pokken Tournament" + class BATTLE_TYPE(Enum): + BATTLE_TYPE_TUTORIAL = 1 + BATTLE_TYPE_AI = 2 + BATTLE_TYPE_LAN = 3 + BATTLE_TYPE_WAN = 4 + + class BATTLE_RESULT(Enum): + BATTLE_RESULT_WIN = 1 + BATTLE_RESULT_LOSS = 2 + @classmethod def game_ver_to_string(cls, ver: int): return cls.VERSION_NAMES[ver] diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 37729fc..871ff9d 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from ..const import PokkenConstants # Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me # Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue @@ -67,7 +68,6 @@ profile = Table( Column('wko_num', Integer), Column('timeup_win_num', Integer), Column('cool_ko_num', Integer), - Column('cool_ko_num', Integer), Column('perfect_ko_num', Integer), Column('record_flag', Integer), Column('continue_num', Integer), @@ -92,7 +92,14 @@ profile = Table( Column('last_play_event_id', Integer), # Optional Column('event_achievement_flag', JSON), # Repeated, Integer Column('event_achievement_param', JSON), # Repeated, Integer - mysql_charset="utf8mb4", + Column('battle_num_vs_wan', Integer), # 4? + Column('win_vs_wan', Integer), + Column('battle_num_vs_lan', Integer), # 3? + Column('win_vs_lan', Integer), + Column('battle_num_vs_cpu', Integer), # 2 + Column('win_cpu', Integer), + Column('battle_num_tutorial', Integer), # 1? + mysql_charset="utf8mb4" ) pokemon_data = Table( @@ -101,20 +108,22 @@ pokemon_data = Table( Column('id', Integer, primary_key=True, nullable=False), Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column('char_id', Integer, nullable=False), - Column('illustration_book_no', Integer, nullable=False), - Column('pokemon_exp', Integer, nullable=False), - Column('battle_num_vs_wan', Integer, nullable=False), - Column('win_vs_wan', Integer, nullable=False), - Column('battle_num_vs_lan', Integer, nullable=False), - Column('win_vs_lan', Integer, nullable=False), - Column('battle_num_vs_cpu', Integer, nullable=False), - Column('win_cpu', Integer, nullable=False), - Column('battle_all_num_tutorial', Integer, nullable=False), - Column('battle_num_tutorial', Integer, nullable=False), - Column('bp_point_atk', Integer, nullable=False), - Column('bp_point_res', Integer, nullable=False), - Column('bp_point_def', Integer, nullable=False), - Column('bp_point_sp', Integer, nullable=False), + Column('illustration_book_no', Integer), + Column('pokemon_exp', Integer), + Column('battle_num_vs_wan', Integer), # 4? + Column('win_vs_wan', Integer), + Column('battle_num_vs_lan', Integer), # 3? + Column('win_vs_lan', Integer), + Column('battle_num_vs_cpu', Integer), # 2 + Column('win_cpu', Integer), + Column('battle_all_num_tutorial', Integer), + Column('battle_num_tutorial', Integer), # 1? + Column('bp_point_atk', Integer), + Column('bp_point_res', Integer), + Column('bp_point_def', Integer), + Column('bp_point_sp', Integer), + UniqueConstraint('user', 'char_id', name="pokken_pokemon_data_uk"), + mysql_charset="utf8mb4" ) class PokkenProfileData(BaseData): @@ -146,7 +155,7 @@ class PokkenProfileData(BaseData): if result is None: return None return result.fetchone() - def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: + def put_pokemon_data(self, user_id: int, pokemon_id: int, illust_no: int, get_exp: int, atk: int, res: int, defe: int, sp: int) -> Optional[int]: pass def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: @@ -154,3 +163,15 @@ class PokkenProfileData(BaseData): def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: pass + + def put_results(self, user_id: int, pokemon_id: int, match_type: int, match_result: int) -> None: + """ + Records the match stats (type and win/loss) for the pokemon and profile + """ + pass + + def put_stats(self, user_id: int, exkos: int, wkos: int, timeout_wins: int, cool_kos: int, perfects: int, continues: int) -> None: + """ + Records profile stats + """ + pass From b1f9be0121ac4ceb257a1cf1e7b4feb2f62add59 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 14 Apr 2023 23:47:31 -0400 Subject: [PATCH 167/589] wacca: fix crash on 4th page of Reverse Gate, partially fixes #5 --- core/data/schema/versions/SDFE_3_rollback.sql | 3 +++ core/data/schema/versions/SDFE_4_upgrade.sql | 3 +++ titles/wacca/__init__.py | 2 +- titles/wacca/base.py | 3 --- 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 core/data/schema/versions/SDFE_3_rollback.sql create mode 100644 core/data/schema/versions/SDFE_4_upgrade.sql diff --git a/core/data/schema/versions/SDFE_3_rollback.sql b/core/data/schema/versions/SDFE_3_rollback.sql new file mode 100644 index 0000000..b2310ad --- /dev/null +++ b/core/data/schema/versions/SDFE_3_rollback.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +--Can't really add items back so this just does nothing +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDFE_4_upgrade.sql b/core/data/schema/versions/SDFE_4_upgrade.sql new file mode 100644 index 0000000..03c5d1e --- /dev/null +++ b/core/data/schema/versions/SDFE_4_upgrade.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +DELETE FROM wacca_item WHERE type=17 AND item_id=312002; +SET FOREIGN_KEY_CHECKS=1; diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index b6e06f8..b3f9850 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 +current_schema_version = 4 diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 020c167..bef7ee2 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -275,9 +275,6 @@ class WaccaBase: 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() From 0d5567c9900ff5454bded304b8e87fb58fa8e06e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:12:28 -0400 Subject: [PATCH 168/589] wacca: fix v4 upgrade scripts --- core/data/schema/versions/SDFE_3_rollback.sql | 1 - core/data/schema/versions/SDFE_4_upgrade.sql | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/data/schema/versions/SDFE_3_rollback.sql b/core/data/schema/versions/SDFE_3_rollback.sql index b2310ad..6d1d2b8 100644 --- a/core/data/schema/versions/SDFE_3_rollback.sql +++ b/core/data/schema/versions/SDFE_3_rollback.sql @@ -1,3 +1,2 @@ SET FOREIGN_KEY_CHECKS=0; ---Can't really add items back so this just does nothing SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDFE_4_upgrade.sql b/core/data/schema/versions/SDFE_4_upgrade.sql index 03c5d1e..3f2cc9a 100644 --- a/core/data/schema/versions/SDFE_4_upgrade.sql +++ b/core/data/schema/versions/SDFE_4_upgrade.sql @@ -1,3 +1 @@ -SET FOREIGN_KEY_CHECKS=0; -DELETE FROM wacca_item WHERE type=17 AND item_id=312002; -SET FOREIGN_KEY_CHECKS=1; +DELETE FROM wacca_item WHERE type=17 AND item_id=312002; \ No newline at end of file From baa885f674ffbb91a9bd192d8943569d41bf561b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:12:45 -0400 Subject: [PATCH 169/589] Utils: exclude malformed game folders --- core/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index d18289e..6bd5c08 100644 --- a/core/utils.py +++ b/core/utils.py @@ -16,7 +16,8 @@ class Utils: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - ret[dir] = mod + if hasattr(mod, "game_codes") and hasattr(mod, "index"): # Minimum required to function + ret[dir] = mod except ImportError as e: logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") From a416fb09e1ea3ff53e9d80f1f8af405f4890ea2b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:13:04 -0400 Subject: [PATCH 170/589] dbutils: version can now be left black to auto-upgrade to latest --- core/data/database.py | 27 +++++++++++++++++++++++---- dbutils.py | 11 ++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 07fe79e..c52e61c 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -32,7 +32,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 = 4 + self.current_schema_version = 4 log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" log_fmt = logging.Formatter(log_fmt_str) @@ -84,8 +84,8 @@ class Data: 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 base_schema_ver to {self.current_schema_version}") + self.base.set_schema_ver(self.current_schema_version) self.logger.info( f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}" @@ -129,9 +129,28 @@ class Data: self.create_database() - def migrate_database(self, game: str, version: int, action: str) -> None: + def migrate_database(self, game: str, version: Optional[int], action: str) -> None: old_ver = self.base.get_schema_ver(game) sql = "" + if version is None: + if not game == "CORE": + titles = Utils.get_all_titles() + + for folder, mod in titles.items(): + for game_ in mod.game_codes: + if not game_ == game: continue + + if hasattr(mod, "current_schema_version"): + version = mod.current_schema_version + + else: + self.logger.warn(f"current_schema_version not found for {folder}") + + else: + version = self.current_schema_version + + if version is None: + self.logger.warn(f"Could not determine latest version for {game}, please specify --version") if old_ver is None: self.logger.error( diff --git a/dbutils.py b/dbutils.py index 176c67e..14af98e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,5 +1,6 @@ import yaml import argparse +import logging from core.config import CoreConfig from core.data import Data from os import path, mkdir, access, W_OK @@ -43,6 +44,7 @@ if __name__ == "__main__": ) exit(1) + cfg.update({"database": {"loglevel": "info"}}) # Force it to be info if we're doing db work data = Data(cfg) @@ -54,15 +56,14 @@ if __name__ == "__main__": elif args.action == "upgrade" or args.action == "rollback": if args.version is None: - data.logger.error("Must set game and version to migrate to") - exit(0) + data.logger.warn("No version set, upgrading to latest") if args.game is None: - data.logger.info("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version), args.action) + data.logger.warn("No game set, upgrading core schema") + data.migrate_database("CORE", int(args.version) if args.version is not None else None, args.action) else: - data.migrate_database(args.game, int(args.version), args.action) + data.migrate_database(args.game, int(args.version) if args.version is not None else None, args.action) elif args.action == "autoupgrade": data.autoupgrade() From 44193100866fa89bed24f5ffc7e773d142262d39 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 01:31:40 -0400 Subject: [PATCH 171/589] fix schema versions for diva and ongeki --- titles/diva/__init__.py | 2 +- titles/ongeki/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index 9d93468..3f193db 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 +current_schema_version = 4 diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index ddde049..1ba901b 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 +current_schema_version = 4 From 98950681253fe062ea8c84f51e1093dc8fda664f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 01:31:52 -0400 Subject: [PATCH 172/589] database: fix autoupdate --- core/data/database.py | 63 ++++++++++++++++++++++++++++------------ core/data/schema/base.py | 2 +- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index c52e61c..12eee8e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,5 +1,5 @@ import logging, coloredlogs -from typing import Optional +from typing import Optional, Dict, List from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine @@ -137,14 +137,13 @@ class Data: titles = Utils.get_all_titles() for folder, mod in titles.items(): - for game_ in mod.game_codes: - if not game_ == game: continue - - if hasattr(mod, "current_schema_version"): - version = mod.current_schema_version - - else: - self.logger.warn(f"current_schema_version not found for {folder}") + if not mod.game_codes[0] == game: continue + + if hasattr(mod, "current_schema_version"): + version = mod.current_schema_version + + else: + self.logger.warn(f"current_schema_version not found for {folder}") else: version = self.current_schema_version @@ -282,17 +281,45 @@ class Data: self.user.delete_user(user["id"]) def autoupgrade(self) -> None: - all_games = self.base.get_all_schema_vers() - if all_games is None: + all_game_versions = self.base.get_all_schema_vers() + if all_game_versions is None: self.logger.warn("Failed to get schema versions") + return - for x in all_games: + print(all_game_versions) + + all_games = Utils.get_all_titles() + all_games_list: Dict[str, int] = {} + for _, mod in all_games.items(): + if hasattr(mod, "current_schema_version"): + all_games_list[mod.game_codes[0]] = mod.current_schema_version + + for x in all_game_versions: + game = x["game"].upper() - update_ver = 1 - for y in range(2, 100): + update_ver = int(x["version"]) + latest_ver = all_games_list.get(game, 1) + if game == "CORE": + latest_ver = self.current_schema_version + + if update_ver == latest_ver: + self.logger.info(f"{game} is already latest version") + continue + + for y in range(update_ver + 1, latest_ver + 1): if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): - update_ver = y + with open( + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error(f"Error execuing sql script for game {game} v{y}!") + continue else: - break - - self.migrate_database(game, update_ver, "upgrade") \ No newline at end of file + self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + + self.base.set_schema_ver(latest_ver, game) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index f77a9aa..ea2fc88 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -47,7 +47,7 @@ class BaseData: res = None try: - self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())} || {opts}") + self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}") res = self.conn.execute(text(sql), opts) except SQLAlchemyError as e: From 83d2151b6bf36175805d94ab11bed748106339aa Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:06:11 -0400 Subject: [PATCH 173/589] dbutils: fix config loading incorrectly --- dbutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dbutils.py b/dbutils.py index 14af98e..3be3ebe 100644 --- a/dbutils.py +++ b/dbutils.py @@ -33,7 +33,9 @@ if __name__ == "__main__": cfg = CoreConfig() if path.exists(f"{args.config}/core.yaml"): - cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) + cfg_dict.get('database', {})['loglevel'] = 'info' + cfg.update(cfg_dict) if not path.exists(cfg.server.log_dir): mkdir(cfg.server.log_dir) @@ -44,7 +46,6 @@ if __name__ == "__main__": ) exit(1) - cfg.update({"database": {"loglevel": "info"}}) # Force it to be info if we're doing db work data = Data(cfg) From 4102ba21fcd1148a975b07eb29a8f641d1a879a2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:07:15 -0400 Subject: [PATCH 174/589] database: remove print --- core/data/database.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 12eee8e..bcfcb1c 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -286,8 +286,6 @@ class Data: self.logger.warn("Failed to get schema versions") return - print(all_game_versions) - all_games = Utils.get_all_titles() all_games_list: Dict[str, int] = {} for _, mod in all_games.items(): From 0dc96f33e110ce69c0495e29b0898ee59578c465 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:13:14 -0400 Subject: [PATCH 175/589] database: don't set schema version if autoupdate fails --- core/data/database.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index bcfcb1c..40bbf33 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -293,7 +293,7 @@ class Data: all_games_list[mod.game_codes[0]] = mod.current_schema_version for x in all_game_versions: - + failed = False game = x["game"].upper() update_ver = int(x["version"]) latest_ver = all_games_list.get(game, 1) @@ -316,8 +316,10 @@ class Data: result = self.base.execute(sql) if result is None: self.logger.error(f"Error execuing sql script for game {game} v{y}!") - continue + failed = True + break else: self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + failed = True - self.base.set_schema_ver(latest_ver, game) + if not failed: self.base.set_schema_ver(latest_ver, game) From 469ead7a842f0a3920299f20468849ba45a3f2b2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 18 Apr 2023 02:52:41 -0400 Subject: [PATCH 176/589] wacca: add previously disabled gates, for #15 --- example_config/wacca.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example_config/wacca.yaml b/example_config/wacca.yaml index aea4f16..bd96795 100644 --- a/example_config/wacca.yaml +++ b/example_config/wacca.yaml @@ -29,5 +29,8 @@ gates: - 17 - 18 - 19 + - 20 - 21 - 22 + - 23 + - 24 From b0042bc776b2abcf0b4f44b2cd74aa2cc3b520bb Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 10:50:40 -0400 Subject: [PATCH 177/589] docs: update in accordance with new dbutils --- docs/game_specific_info.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index b09d61e..7700da5 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -64,8 +64,7 @@ which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to v perform all previous updates as well: ```shell -python dbutils.py --game SDBT --version 2 upgrade -python dbutils.py --game SDBT --version 3 upgrade +python dbutils.py --game SDBT upgrade ``` ## crossbeats REV. @@ -133,7 +132,7 @@ crash without it!** 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 +python dbutils.py --game SDEZ upgrade ``` ## Hatsune Miku Project Diva @@ -174,9 +173,7 @@ which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to v 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 +python dbutils.py --game SBZV upgrade ``` ## O.N.G.E.K.I. @@ -224,9 +221,7 @@ which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to v 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 +python dbutils.py --game SDDT upgrade ``` ## Card Maker @@ -346,6 +341,5 @@ Config file is located in `config/wacca.yaml`. 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 +python dbutils.py --game SDFE upgrade ``` From 15433b681caea701a7611470c1509bc7e2d304b9 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 11:26:33 -0400 Subject: [PATCH 178/589] mai2: fix logging in put_card --- titles/mai2/schema/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d64d954..81e817a 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -438,7 +438,7 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: self.logger.warn( - f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}" + f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}" ) return None return result.lastrowid From 958471b8ebdcad0240bfddb947bd49ac725a0d86 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 19 Apr 2023 17:41:36 +0200 Subject: [PATCH 179/589] mai2: update script hotfix --- core/data/schema/versions/SDEZ_3_rollback.sql | 6 +++--- core/data/schema/versions/SDEZ_4_upgrade.sql | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql index f819855..79ca098 100644 --- a/core/data/schema/versions/SDEZ_3_rollback.sql +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -20,12 +20,12 @@ CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; ALTER TABLE mai2_item_friend_season_ranking -CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN seasonId season_id INT NOT NULL, CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonusId bonus_id INT NOT NULL, -CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, -CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file +CHANGE COLUMN isCurrent is_current BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql index 52d120a..a670d6a 100644 --- a/core/data/schema/versions/SDEZ_4_upgrade.sql +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -27,5 +27,5 @@ CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonus_id bonusId INT NOT NULL, -CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, -CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; +CHANGE COLUMN is_current isCurrent BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; From 68b0894e477b56bd41b19e0214305f365301ab50 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 16:12:35 -0400 Subject: [PATCH 180/589] wacca: fix first play of the day calculation --- titles/wacca/base.py | 32 ++++++++++++-------------------- titles/wacca/lily.py | 28 +++++++--------------------- 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index bef7ee2..d4bf873 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -183,8 +183,6 @@ class WaccaBase: def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() - is_new_day = False - is_consec_day = False is_consec_day = True if req.userId == 0: @@ -202,29 +200,23 @@ class WaccaBase: 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 + midnight_today_ts = 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() - ): + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) + self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) + + if resp.firstLoginDaily: + # TODO: Daily bonus + pass - resp.firstLoginDaily = int(is_new_day) + # TODO: VIP dialy/monthly rewards return resp.make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index c3b6eb4..122cd47 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -143,8 +143,6 @@ class WaccaLily(WaccaS): def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() - is_new_day = False - is_consec_day = False is_consec_day = True if req.userId == 0: @@ -162,34 +160,22 @@ class WaccaLily(WaccaS): 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 + midnight_today_ts = 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() - ): + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) + self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month resp.vipInfo.pageDay = datetime.now().day resp.vipInfo.numItem = 1 - resp.firstLoginDaily = int(is_new_day) - return resp.make() def handle_user_status_getDetail_request(self, data: Dict) -> Dict: From 4d6afd757f3257bfb7b695f6765c9b21b481d1b8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 00:54:16 -0400 Subject: [PATCH 181/589] wacca: add helpers for gacha, event and friend info, fix settings not being applied correctly --- titles/wacca/base.py | 4 +- titles/wacca/handlers/helpers.py | 87 +++++++++++++++++++++------- titles/wacca/handlers/housing.py | 6 +- titles/wacca/handlers/user_status.py | 22 +++++-- titles/wacca/lily.py | 5 ++ 5 files changed, 93 insertions(+), 31 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index d4bf873..5663588 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -825,7 +825,7 @@ class WaccaBase: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - resp.songDetail.lock_state = 1 + resp.songDetail.lockState = 1 return resp.make() # TODO: Coop and vs data @@ -971,7 +971,7 @@ class WaccaBase: user_id = self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: - self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val) + self.data.profile.update_option(user_id, opt.optId, opt.optVal) for update in req.datesUpdated: pass diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index f86be2f..dc836ce 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -158,11 +158,11 @@ class Notice: class UserOption: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: - self.opt_id = opt_id - self.opt_val = opt_val + self.optId = opt_id + self.optVal = opt_val def make(self) -> List: - return [self.opt_id, self.opt_val] + return [self.optId, self.optVal] class UserStatusV1: @@ -348,13 +348,35 @@ class NavigatorItem(IconItem): class SkillItem: - skill_type: int + skillType: int level: int flag: int badge: int def make(self) -> List: - return [self.skill_type, self.level, self.flag, self.badge] + return [self.skillType, self.level, self.flag, self.badge] + + +class UserEventInfo: + def __init__(self) -> None: + self.eventId = 0 + self.conditionInfo: List[UserEventConditionInfo] = [] + + def make(self) -> List: + conditions = [] + for x in self.conditionInfo: + conditions.append(x.make()) + + return [self.eventId, conditions] + + +class UserEventConditionInfo: + def __init__(self) -> None: + self.achievementCondition = 0 + self.progress = 0 + + def make(self) -> List: + return [self.achievementCondition, self.progress] class UserItemInfoV1: @@ -447,19 +469,19 @@ class UserItemInfoV3(UserItemInfoV2): 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, + playCt: int = 0, + clearCt: int = 0, + mlCt: int = 0, + fcCt: int = 0, + amCt: int = 0, counts: Optional[List[int]] = None, ) -> None: if counts is None: - self.playCt = play_ct - self.clearCt = clear_ct - self.misslessCt = ml_ct - self.fullComboCt = fc_ct - self.allMarvelousCt = am_ct + self.playCt = playCt + self.clearCt = clearCt + self.misslessCt = mlCt + self.fullComboCt = fcCt + self.allMarvelousCt = amCt else: self.playCt = counts[0] @@ -773,8 +795,12 @@ class GateDetailV2(GateDetailV1): class GachaInfo: + def __init__(self, gacha_id: int = 0, gacha_roll_ct: int = 0) -> None: + self.gachaId = gacha_id + self.rollCt = gacha_roll_ct + def make(self) -> List: - return [] + return [self.gachaId, self.rollCt] class LastSongDetail: @@ -808,9 +834,30 @@ class LastSongDetail: ] -class FriendDetail: +class FriendScoreDetail: + def __init__(self, song_id: int = 0, difficulty: int = 1, best_score: int = 0) -> None: + self.songId = song_id + self.difficulty = difficulty + self.bestScore = best_score + def make(self) -> List: - return [] + return [self.songId, self.difficulty, self.bestScore] + + +class FriendDetail: + def __init__(self, user_id: int = 0, username: str = "") -> None: + self.friendId = user_id + self.friendUsername = username + self.friendUserType = 1 + self.friendScoreDetail: List[FriendScoreDetail] = [] + + def make(self) -> List: + scores = [] + + for x in self.friendScoreDetail: + scores.append(x.make()) + + return [self.friendId, self.friendUsername, self.friendUserType, scores] class LoginBonusInfo: @@ -942,7 +989,7 @@ class MusicUpdateDetailV1: self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.lock_state = 0 + self.lockState = 0 def make(self) -> List: return [ @@ -954,7 +1001,7 @@ class MusicUpdateDetailV1: self.score, self.lowestMissCount, self.maxSkillPts, - self.lock_state, + self.lockState, ] diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 8ffa910..f2f079e 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -10,10 +10,10 @@ class HousingGetResponse(BaseResponse): def __init__(self, housingId: int) -> None: super().__init__() self.housingId: int = housingId - self.regionId: int = 0 + self.isNewCab: bool = False def make(self) -> Dict: - self.params = [self.housingId, self.regionId] + self.params = [self.housingId, int(self.isNewCab)] return super().make() @@ -32,8 +32,6 @@ class HousingStartRequestV1(BaseRequest): class HousingStartRequestV2(HousingStartRequestV1): def __init__(self, data: Dict) -> None: super(HousingStartRequestV1, self).__init__(data) - self.unknown0: str = self.params[0] - self.errorLog: str = self.params[1] self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 0e3819d..b09fed6 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -39,12 +39,16 @@ class UserStatusGetV2Response(UserStatusGetV1Response): def __init__(self) -> None: super().__init__() self.userStatus: UserStatusV2 = UserStatusV2() - self.unknownArr: List = [] + self.options: List[UserOption] = [] def make(self) -> Dict: super().make() + opts = [] - self.params.append(self.unknownArr) + for x in self.options: + opts.append(x.make()) + + self.params.append(opts) return super(UserStatusGetV1Response, self).make() @@ -137,7 +141,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): self.userItems: UserItemInfoV2 = UserItemInfoV2() self.favorites: List[int] = [] self.stoppedSongIds: List[int] = [] - self.eventInfo: List[int] = [] + self.eventInfo: List[UserEventInfo] = [] self.gateInfo: List[GateDetailV1] = [] self.lastSongInfo: LastSongDetail = LastSongDetail() self.gateTutorialFlags: List[GateTutorialFlag] = [] @@ -149,6 +153,8 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): gates = [] friends = [] tut_flg = [] + evts = [] + gacha = [] for x in self.gateInfo: gates.append(x.make()) @@ -162,14 +168,20 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) + + for x in self.eventInfo: + evts.append(x.make()) + + for x in self.gatchaInfo: + gacha.append(x.make()) self.params.append(self.favorites) self.params.append(self.stoppedSongIds) - self.params.append(self.eventInfo) + self.params.append(evts) self.params.append(gates) self.params.append(self.lastSongInfo.make()) self.params.append(tut_flg) - self.params.append(self.gatchaInfo) + self.params.append(gacha) self.params.append(friends) return super(UserStatusGetDetailResponseV1, self).make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 122cd47..4ff942b 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -73,6 +73,8 @@ class WaccaLily(WaccaS): self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() + + opts = self.data.profile.get_options(req.aimeId) self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: @@ -137,6 +139,9 @@ class WaccaLily(WaccaS): if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 + + for opt in opts: + resp.options.append(UserOption(opt["opt_id"], opt["value"])) return resp.make() From 241f29e29c25506b23e43acf46338ea87057a1f7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 02:02:42 -0400 Subject: [PATCH 182/589] wacca: add a comment --- titles/wacca/handlers/user_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index b09fed6..b7ed1fc 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -267,7 +267,7 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2): self, is_first_login_daily: bool = False, last_login_date: int = 0 ) -> None: super().__init__(is_first_login_daily, last_login_date) - self.unk: List = [] + self.unk: List = [] # Ticket info, item info, message, title, voice name (not sure how they fit...) def make(self) -> Dict: super().make() From 00b127361b35a301e1817d7a00760f5f35c0b9ac Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 02:27:09 -0400 Subject: [PATCH 183/589] wacca: enable time play --- titles/wacca/reverse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index f32b0c4..ef14ecb 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -137,6 +137,9 @@ class WaccaReverse(WaccaLilyR): PlayModeCounts(self.season, 4, profile["playcount_stageup"]) ) + # For some fucking reason if this isn't here time play is disabled + resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) From a30967e8d79ea0defeb06dcacfb62571d525ff3f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 09:46:18 -0400 Subject: [PATCH 184/589] wacca: fix time free not saving, add counter to profile table --- core/data/schema/versions/SDFE_4_rollback.sql | 1 + core/data/schema/versions/SDFE_5_upgrade.sql | 1 + titles/wacca/__init__.py | 2 +- titles/wacca/handlers/helpers.py | 1 + titles/wacca/schema/profile.py | 13 +++++++++---- 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 core/data/schema/versions/SDFE_4_rollback.sql create mode 100644 core/data/schema/versions/SDFE_5_upgrade.sql diff --git a/core/data/schema/versions/SDFE_4_rollback.sql b/core/data/schema/versions/SDFE_4_rollback.sql new file mode 100644 index 0000000..417c227 --- /dev/null +++ b/core/data/schema/versions/SDFE_4_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile DROP COLUMN playcount_time_free; \ No newline at end of file diff --git a/core/data/schema/versions/SDFE_5_upgrade.sql b/core/data/schema/versions/SDFE_5_upgrade.sql new file mode 100644 index 0000000..a9795bf --- /dev/null +++ b/core/data/schema/versions/SDFE_5_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup; \ No newline at end of file diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index b3f9850..a3bf96b 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 = 4 +current_schema_version = 5 diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index dc836ce..b17602a 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -941,6 +941,7 @@ class PlayType(Enum): PlayTypeVs = 2 PlayTypeCoop = 3 PlayTypeStageup = 4 + PlayTypeTimeFree = 5 class StageInfo: diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index 27111be..48eb800 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from ..handlers.helpers import PlayType profile = Table( "wacca_profile", @@ -40,6 +41,7 @@ profile = Table( Column("playcount_multi_vs", Integer, server_default="0"), Column("playcount_multi_coop", Integer, server_default="0"), Column("playcount_stageup", Integer, server_default="0"), + Column("playcount_time_free", Integer, server_default="0"), Column("friend_view_1", Integer), Column("friend_view_2", Integer), Column("friend_view_3", Integer), @@ -160,17 +162,20 @@ class WaccaProfileData(BaseData): ) -> None: sql = profile.update(profile.c.id == profile_id).values( playcount_single=profile.c.playcount_single + 1 - if play_type == 1 + if play_type == PlayType.PlayTypeSingle.value else profile.c.playcount_single, playcount_multi_vs=profile.c.playcount_multi_vs + 1 - if play_type == 2 + if play_type == PlayType.PlayTypeVs.value else profile.c.playcount_multi_vs, playcount_multi_coop=profile.c.playcount_multi_coop + 1 - if play_type == 3 + if play_type == PlayType.PlayTypeCoop.value else profile.c.playcount_multi_coop, playcount_stageup=profile.c.playcount_stageup + 1 - if play_type == 4 + if play_type == PlayType.PlayTypeStageup.value else profile.c.playcount_stageup, + playcount_time_free=profile.c.playcount_time_free + 1 + if play_type == PlayType.PlayTypeTimeFree.value + else profile.c.playcount_time_free, last_game_ver=game_version, ) From 190c41e03e31d7e8e703c80bacf341e745b26ae9 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 21 Apr 2023 10:51:30 -0400 Subject: [PATCH 185/589] wacca: added play mode counter for time free --- titles/wacca/reverse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index ef14ecb..bb8a332 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -136,6 +136,9 @@ class WaccaReverse(WaccaLilyR): resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 4, profile["playcount_stageup"]) ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 5, profile["playcount_time_free"]) + ) # For some fucking reason if this isn't here time play is disabled resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) From 58a088b9a47a3da34cfe9f2aead6605732a2f21d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 21 Apr 2023 10:51:44 -0400 Subject: [PATCH 186/589] wacca: add debug log for ticket use --- titles/wacca/base.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 5663588..35a6efc 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -634,6 +634,7 @@ class WaccaBase: 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.logger.debug(f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}") self.data.item.spend_ticket(new_tickets[x][0]) new_tickets.pop(x) break @@ -657,13 +658,8 @@ class WaccaBase: ) if self.game_config.mods.infinite_tickets: - new_tickets = [ - [0, 106002, 0], - [1, 106002, 0], - [2, 106002, 0], - [3, 106002, 0], - [4, 106002, 0], - ] + for x in range(5): + new_tickets.append(TicketItem(x, 106002, 0)) if self.game_config.mods.infinite_wp: current_wp = 999999 From d8c3ed5c013c96001da2e931c7419564356c4bd7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 04:38:28 -0400 Subject: [PATCH 187/589] Add support for initial d zero --- example_config/idz.yaml | 11 ++ titles/idz/__init__.py | 8 + titles/idz/config.py | 67 ++++++++ titles/idz/const.py | 40 +++++ titles/idz/database.py | 6 + titles/idz/echo.py | 16 ++ titles/idz/handlers/__init__.py | 39 +++++ titles/idz/handlers/base.py | 25 +++ titles/idz/handlers/check_team_names.py | 19 ++ titles/idz/handlers/create_auto_team.py | 49 ++++++ titles/idz/handlers/create_profile.py | 91 ++++++++++ titles/idz/handlers/create_team.py | 0 titles/idz/handlers/discover_profile.py | 23 +++ titles/idz/handlers/load_2on2.py | 36 ++++ titles/idz/handlers/load_config.py | 41 +++++ titles/idz/handlers/load_ghost.py | 39 +++++ titles/idz/handlers/load_profile.py | 28 +++ titles/idz/handlers/load_reward_table.py | 17 ++ titles/idz/handlers/load_server_info.py | 60 +++++++ titles/idz/handlers/load_team_ranking.py | 29 ++++ titles/idz/handlers/load_top_ten.py | 30 ++++ titles/idz/handlers/lock_profile.py | 39 +++++ titles/idz/handlers/save_expedition.py | 14 ++ titles/idz/handlers/save_profile.py | 15 ++ titles/idz/handlers/save_topic.py | 17 ++ titles/idz/handlers/unknown.py | 12 ++ .../handlers/update_provisional_store_rank.py | 20 +++ titles/idz/handlers/update_story_clear_num.py | 26 +++ titles/idz/index.py | 146 ++++++++++++++++ titles/idz/match.py | 0 titles/idz/userdb.py | 162 ++++++++++++++++++ 31 files changed, 1125 insertions(+) create mode 100644 example_config/idz.yaml create mode 100644 titles/idz/__init__.py create mode 100644 titles/idz/config.py create mode 100644 titles/idz/const.py create mode 100644 titles/idz/database.py create mode 100644 titles/idz/echo.py create mode 100644 titles/idz/handlers/__init__.py create mode 100644 titles/idz/handlers/base.py create mode 100644 titles/idz/handlers/check_team_names.py create mode 100644 titles/idz/handlers/create_auto_team.py create mode 100644 titles/idz/handlers/create_profile.py create mode 100644 titles/idz/handlers/create_team.py create mode 100644 titles/idz/handlers/discover_profile.py create mode 100644 titles/idz/handlers/load_2on2.py create mode 100644 titles/idz/handlers/load_config.py create mode 100644 titles/idz/handlers/load_ghost.py create mode 100644 titles/idz/handlers/load_profile.py create mode 100644 titles/idz/handlers/load_reward_table.py create mode 100644 titles/idz/handlers/load_server_info.py create mode 100644 titles/idz/handlers/load_team_ranking.py create mode 100644 titles/idz/handlers/load_top_ten.py create mode 100644 titles/idz/handlers/lock_profile.py create mode 100644 titles/idz/handlers/save_expedition.py create mode 100644 titles/idz/handlers/save_profile.py create mode 100644 titles/idz/handlers/save_topic.py create mode 100644 titles/idz/handlers/unknown.py create mode 100644 titles/idz/handlers/update_provisional_store_rank.py create mode 100644 titles/idz/handlers/update_story_clear_num.py create mode 100644 titles/idz/index.py create mode 100644 titles/idz/match.py create mode 100644 titles/idz/userdb.py diff --git a/example_config/idz.yaml b/example_config/idz.yaml new file mode 100644 index 0000000..1bfff9b --- /dev/null +++ b/example_config/idz.yaml @@ -0,0 +1,11 @@ +server: + enable: True + loglevel: "info" + hostname: "" + news: "" + aes_key: "" + +ports: + userdb: 10000 + match: 10010 + echo: 10020 diff --git a/titles/idz/__init__.py b/titles/idz/__init__.py new file mode 100644 index 0000000..958d08a --- /dev/null +++ b/titles/idz/__init__.py @@ -0,0 +1,8 @@ +from titles.idz.index import IDZServlet +from titles.idz.const import IDZConstants +from titles.idz.database import IDZData + +index = IDZServlet +database = IDZData +game_codes = [IDZConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idz/config.py b/titles/idz/config.py new file mode 100644 index 0000000..2ff4e5c --- /dev/null +++ b/titles/idz/config.py @@ -0,0 +1,67 @@ +from typing import List, Dict + +from core.config import CoreConfig + + +class IDZServerConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "idz", "server", "loglevel", default="info" + ) + ) + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "hostname", default="" + ) + + @property + def news(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "news", default="" + ) + + @property + def aes_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "aes_key", default="" + ) + + +class IDZPortsConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def userdb(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "userdb", default=10000) + + @property + def match(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "match", default=10010) + + @property + def echo(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "echo", default=10020) + + +class IDZConfig(dict): + def __init__(self) -> None: + self.server = IDZServerConfig(self) + self.ports = IDZPortsConfig(self) + + @property + def rsa_keys(self) -> List[Dict]: + return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) \ No newline at end of file diff --git a/titles/idz/const.py b/titles/idz/const.py new file mode 100644 index 0000000..164c940 --- /dev/null +++ b/titles/idz/const.py @@ -0,0 +1,40 @@ +from enum import Enum + +class IDZConstants: + GAME_CODE = "SDDF" + + CONFIG_NAME = "idz.yaml" + + VER_IDZ_110 = 0 + VER_IDZ_130 = 1 + VER_IDZ_210 = 2 + VER_IDZ_230 = 3 + NUM_VERS = 4 + + VERSION_NAMES = ( + "Initial D Arcade Stage Zero v1.10", + "Initial D Arcade Stage Zero v1.30", + "Initial D Arcade Stage Zero v2.10", + "Initial D Arcade Stage Zero v2.30", + ) + + class PROFILE_STATUS(Enum): + LOCKED = 0 + UNLOCKED = 1 + OLD = 2 + + HASH_LUT = [ + # No clue + 0x9C82E674, 0x5A4738D9, 0x8B8D7AE0, 0x29EC9D81, + # These three are from AES TE0 + 0x1209091B, 0x1D83839E, 0x582C2C74, 0x341A1A2E, + 0x361B1B2D, 0xDC6E6EB2, 0xB45A5AEE, 0x5BA0A0FB, + 0xA45252F6, 0x763B3B4D, 0xB7D6D661, 0x7DB3B3CE, + ] + HASH_NUM = 0 + HASH_MUL = [5, 7, 11, 12][HASH_NUM] + HASH_XOR = [0xB3, 0x8C, 0x14, 0x50][HASH_NUM] + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/idz/database.py b/titles/idz/database.py new file mode 100644 index 0000000..1f5a8e2 --- /dev/null +++ b/titles/idz/database.py @@ -0,0 +1,6 @@ +from core.data import Data +from core.config import CoreConfig + +class IDZData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) diff --git a/titles/idz/echo.py b/titles/idz/echo.py new file mode 100644 index 0000000..4414b70 --- /dev/null +++ b/titles/idz/echo.py @@ -0,0 +1,16 @@ +from twisted.internet.protocol import DatagramProtocol +import logging + +from core.config import CoreConfig +from .config import IDZConfig + +class IDZEcho(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idz") + + def datagramReceived(self, data, addr): + self.logger.debug(f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}") + self.transport.write(data, addr) \ No newline at end of file diff --git a/titles/idz/handlers/__init__.py b/titles/idz/handlers/__init__.py new file mode 100644 index 0000000..a3cc248 --- /dev/null +++ b/titles/idz/handlers/__init__.py @@ -0,0 +1,39 @@ +from .base import IDZHandlerBase + +from .load_server_info import IDZHandlerLoadServerInfo + +from .load_ghost import IDZHandlerLoadGhost + +from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB + +from .load_top_ten import IDZHandlerLoadTopTen + +from .update_story_clear_num import IDZHandlerUpdateStoryClearNum + +from .save_expedition import IDZHandlerSaveExpedition + +from .load_2on2 import IDZHandlerLoad2on2A, IDZHandlerLoad2on2B + +from .load_team_ranking import IDZHandlerLoadTeamRankingA, IDZHandlerLoadTeamRankingB + +from .discover_profile import IDZHandlerDiscoverProfile + +from .lock_profile import IDZHandlerLockProfile + +from .check_team_names import IDZHandlerCheckTeamName + +from .unknown import IDZHandlerUnknown + +from .create_profile import IDZHandlerCreateProfile + +from .create_auto_team import IDZHandlerCreateAutoTeam + +from .load_profile import IDZHandlerLoadProfile + +from .save_profile import IDZHandlerSaveProfile + +from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank + +from .load_reward_table import IDZHandlerLoadRewardTable + +from .save_topic import IDZHandlerSaveTopic diff --git a/titles/idz/handlers/base.py b/titles/idz/handlers/base.py new file mode 100644 index 0000000..720f097 --- /dev/null +++ b/titles/idz/handlers/base.py @@ -0,0 +1,25 @@ +import logging +import struct +from core.data import Data +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerBase(): + name = "generic" + cmd_codes = [0x0000] * IDZConstants.NUM_VERS + rsp_codes = [0x0001] * IDZConstants.NUM_VERS + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + self.core_config = core_cfg + self.game_cfg = game_cfg + self.data = Data(core_cfg) + self.logger = logging.getLogger("idz") + self.game = IDZConstants.GAME_CODE + self.version = version + self.size = 0x30 + + def handle(self, data: bytes) -> bytearray: + ret = bytearray([0] * self.size) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ca0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + + aime_id = struct.unpack_from(" 2: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + user_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04c0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x1290 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + +class IDZHandlerLoad2on2B(IDZHandlerBase): + cmd_codes = [0x0132] * 4 + rsp_codes = [0x0133] * 4 + name = "load_2on2B" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04c0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x0540 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py new file mode 100644 index 0000000..fa103af --- /dev/null +++ b/titles/idz/handlers/load_config.py @@ -0,0 +1,41 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerLoadConfigA(IDZHandlerBase): + cmd_codes = [0x0004] * IDZConstants.NUM_VERS + rsp_codes = [0x0005] * IDZConstants.NUM_VERS + name = "load_config_a" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01a0 + + if self.version > 1: + self.size = 0x05e0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0230 + + if self.version > 1: + self.size = 0x0240 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0070 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0d30 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x0ea0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x1360 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x1640 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01c0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py new file mode 100644 index 0000000..3b60f00 --- /dev/null +++ b/titles/idz/handlers/load_server_info.py @@ -0,0 +1,60 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerLoadServerInfo(IDZHandlerBase): + cmd_codes = [0x0006] * IDZConstants.NUM_VERS + rsp_codes = [0x0007] * IDZConstants.NUM_VERS + name = "load_server_info1" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04b0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + offset = 0 + if self.version >= IDZConstants.VER_IDZ_210: + offset = 2 + + news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" + err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" + + len_hostname = len(self.core_config.title.hostname) + len_news = len(news_str) + len_error = len(err_str) + + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ba0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + +class IDZHandlerLoadTeamRankingB(IDZHandlerBase): + cmd_codes = [0x00bb, 0x00bb, 0x00a9, 0x00a9] + rsp_codes = [0x00a8] * 4 + name = "load_team_ranking_b" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ba0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_top_ten.py b/titles/idz/handlers/load_top_ten.py new file mode 100644 index 0000000..ef2a158 --- /dev/null +++ b/titles/idz/handlers/load_top_ten.py @@ -0,0 +1,30 @@ +import struct +from typing import Tuple, List, Dict + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerLoadTopTen(IDZHandlerBase): + cmd_codes = [0x012c] * 4 + rsp_codes = [0x00ce] * 4 + name = "load_top_ten" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x1720 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + tracks_dates: List[Tuple[int, int]] = [] + for i in range(32): + tracks_dates.append( + (struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + profile_data = { + "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, + "expire_time": int((datetime.now() + timedelta(hours=1)).timestamp() / 1000) + } + user_id = struct.unpack_from(" 0: + old_profile = None + if old_profile is not None: + profile_data["status"] = IDZConstants.PROFILE_STATUS.OLD.value + + return self.handle_common(profile_data, ret) + + def handle_common(cls, data: Dict, ret: bytearray) -> bytearray: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_profile.py b/titles/idz/handlers/save_profile.py new file mode 100644 index 0000000..652dbd6 --- /dev/null +++ b/titles/idz/handlers/save_profile.py @@ -0,0 +1,15 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerSaveProfile(IDZHandlerBase): + cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143] + name = "save_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_topic.py b/titles/idz/handlers/save_topic.py new file mode 100644 index 0000000..9499385 --- /dev/null +++ b/titles/idz/handlers/save_topic.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerSaveTopic(IDZHandlerBase): + cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091] + rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092] + name = "save_topic" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x05d0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/unknown.py b/titles/idz/handlers/unknown.py new file mode 100644 index 0000000..ee3046a --- /dev/null +++ b/titles/idz/handlers/unknown.py @@ -0,0 +1,12 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerUnknown(IDZHandlerBase): + cmd_codes = [0x00ad, 0x00ad, 0x00a2, 0x00a2] + name = "unknown" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) diff --git a/titles/idz/handlers/update_provisional_store_rank.py b/titles/idz/handlers/update_provisional_store_rank.py new file mode 100644 index 0000000..cbe344d --- /dev/null +++ b/titles/idz/handlers/update_provisional_store_rank.py @@ -0,0 +1,20 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerUpdateProvisionalStoreRank(IDZHandlerBase): + cmd_codes = [0x0082, 0x0082, 0x007C, 0x007C] + rsp_codes = [0x0083, 0x0083, 0x007D, 0x007D] + name = "update_provisional_store_ranking" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x02b0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + + def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray: + pass diff --git a/titles/idz/handlers/update_story_clear_num.py b/titles/idz/handlers/update_story_clear_num.py new file mode 100644 index 0000000..46f2689 --- /dev/null +++ b/titles/idz/handlers/update_story_clear_num.py @@ -0,0 +1,26 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerUpdateStoryClearNum(IDZHandlerBase): + cmd_codes = [0x007f, 0x097f, 0x013d, 0x0144] + rsp_codes = [0x0080, 0x013e, 0x013e, 0x0145] + name = "update_story_clear_num" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0220 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x04f0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x0510 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x0800 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/index.py b/titles/idz/index.py new file mode 100644 index 0000000..5b60e5a --- /dev/null +++ b/titles/idz/index.py @@ -0,0 +1,146 @@ +from twisted.web.http import Request +import yaml +import logging +import coloredlogs +from logging.handlers import TimedRotatingFileHandler +from os import path +from typing import Tuple, List +from twisted.internet import reactor, endpoints +from twisted.web import server, resource +import importlib + +from core.config import CoreConfig +from .config import IDZConfig +from .const import IDZConstants +from .userdb import IDZUserDBFactory, IDZUserDBWeb, IDZKey +from .echo import IDZEcho +from .handlers import IDZHandlerLoadConfigB + +class IDZServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + self.logger = logging.getLogger("idz") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] IDZ | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idz"), + encoding="utf8", + when="d", + backupCount=10, + ) + + self.rsa_keys: List[IDZKey] = [] + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + @classmethod + def rsaHashKeyN(cls, data): + hash_ = 0 + for i in data: + hash_ = hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) ^ IDZConstants.HASH_LUT[i & 0xf] + hash_ &= 0xffffffff + return hash_ + + @classmethod + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "", "") + + if len(game_cfg.rsa_keys) <= 0 or not game_cfg.server.aes_key: + logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start") + return (False, "", "") + + hostname = core_cfg.title.hostname if not game_cfg.server.hostname else game_cfg.server.hostname + return ( + True, + f"", + f"{hostname}:{game_cfg.ports.userdb}", + ) + + def setup(self): + for key in self.game_cfg.rsa_keys: + if "N" not in key or "d" not in key or "e" not in key: + self.logger.error(f"Invalid IDZ key {key}") + continue + + hashN = self.rsaHashKeyN(str(key["N"]).encode()) + self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN)) + + if len(self.rsa_keys) <= 0: + self.logger.error("No valid RSA keys provided! IDZ cannot start!") + return + + handler_map = [{} for _ in range(IDZConstants.NUM_VERS)] + handler_mod = mod = importlib.import_module(f"titles.idz.handlers") + + for cls_name in dir(handler_mod): + if cls_name.startswith("__"): + continue + + try: + mod = getattr(handler_mod, cls_name) + mod_cmds: List = getattr(mod, "cmd_codes") + while len(mod_cmds) < IDZConstants.NUM_VERS: + mod_cmds.append(None) + + for i in range(len(mod_cmds)): + if mod_cmds[i] is None: + mod_cmds[i] = mod_cmds[i - 1] + + handler_map[i][mod_cmds[i]] = mod + + except AttributeError as e: + continue + + endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}")\ + .listen(IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map)) + + reactor.listenUDP(self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg)) + + self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") + + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + req_raw = request.content.getvalue() + self.logger.info(f"IDZ POST request: {url_path} - {req_raw}") + return b"" + + def render_GET(self, request: Request, version: int, url_path: str) -> bytes: + self.logger.info(f"IDZ GET request: {url_path}") + request.responseHeaders.setRawHeaders('Content-Type', [b"text/plain; charset=utf-8"]) + request.responseHeaders.setRawHeaders("Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"]) + + news = self.game_cfg.server.news if self.game_cfg.server.news else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + news += "\r\n" + news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news + + return news.encode() diff --git a/titles/idz/match.py b/titles/idz/match.py new file mode 100644 index 0000000..e69de29 diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py new file mode 100644 index 0000000..95a2eb9 --- /dev/null +++ b/titles/idz/userdb.py @@ -0,0 +1,162 @@ +from twisted.internet.protocol import Factory, Protocol +import logging, coloredlogs +from Crypto.Cipher import AES +import struct +from typing import Dict, Optional, List, Type +from twisted.web import server, resource +from twisted.internet import reactor, endpoints +from twisted.web.http import Request +from routes import Mapper +import random +from os import walk +import importlib + +from core.config import CoreConfig +from .database import IDZData +from .config import IDZConfig +from .const import IDZConstants +from .handlers import IDZHandlerBase + +HANDLER_MAP: List[Dict] + +class IDZKey: + def __init__(self, n, d, e, hashN: int) -> None: + self.N = n + self.d = d + self.e = e + self.hashN = hashN + +class IDZUserDBProtocol(Protocol): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, keys: List[IDZKey], handlers: List[Dict]) -> None: + self.logger = logging.getLogger('idz') + self.core_config = core_cfg + self.game_config = game_cfg + self.rsa_keys = keys + self.handlers = handlers + self.static_key = bytes.fromhex(self.game_config.server.aes_key) + self.version = None + self.version_internal = None + self.skip_next = False + + def append_padding(self, data: bytes): + """Appends 0s to the end of the data until it's at the correct size""" + length = struct.unpack_from(" None: + self.logger.debug(f"{self.transport.getPeer().host} Connected") + base = 0 + + for i in range(len(self.static_key) - 1): + shift = 8 * i + byte = self.static_key[i] + + base |= byte << shift + + rsa_key = random.choice(self.rsa_keys) + key_enc: int = pow(base, rsa_key.e, rsa_key.N) + result = key_enc.to_bytes(0x40, "little") + struct.pack(" None: + self.logger.debug( + f"{self.transport.getPeer().host} Disconnected - {reason.value}" + ) + + def dataReceived(self, data: bytes) -> None: + self.logger.debug(f"Receive data {data.hex()}") + crypt = AES.new(self.static_key, AES.MODE_ECB) + data_dec = crypt.decrypt(data) + self.logger.debug(f"Decrypt data {data_dec.hex()}") + + magic = struct.unpack_from(" None: + self.core_config = cfg + self.game_config = game_cfg + self.keys = keys + self.handlers = handlers + + def buildProtocol(self, addr): + return IDZUserDBProtocol(self.core_config, self.game_config, self.keys, self.handlers) + +class IDZUserDBWeb(resource.Resource): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): + super().__init__() + self.isLeaf = True + self.core_config = core_cfg + self.game_config = game_cfg + self.logger = logging.getLogger('idz') + + def render_POST(self, request: Request) -> bytes: + self.logger.info(f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}") + return b"" + + def render_GET(self, request: Request) -> bytes: + self.logger.info(f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}") + return b"" \ No newline at end of file From 26c4bcb466555910fa0832906163816b1c28dea2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 13:26:44 -0400 Subject: [PATCH 188/589] idz: Add requests, fix load_config_b --- titles/idz/handlers/__init__.py | 8 ++++++++ titles/idz/handlers/load_config.py | 2 +- titles/idz/handlers/save_time_attack.py | 22 ++++++++++++++++++++++ titles/idz/handlers/unlock_profile.py | 20 ++++++++++++++++++++ titles/idz/handlers/update_team_points.py | 17 +++++++++++++++++ titles/idz/handlers/update_ui_report.py | 17 +++++++++++++++++ titles/idz/handlers/update_user_log.py | 17 +++++++++++++++++ 7 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 titles/idz/handlers/save_time_attack.py create mode 100644 titles/idz/handlers/unlock_profile.py create mode 100644 titles/idz/handlers/update_team_points.py create mode 100644 titles/idz/handlers/update_ui_report.py create mode 100644 titles/idz/handlers/update_user_log.py diff --git a/titles/idz/handlers/__init__.py b/titles/idz/handlers/__init__.py index a3cc248..17213ba 100644 --- a/titles/idz/handlers/__init__.py +++ b/titles/idz/handlers/__init__.py @@ -37,3 +37,11 @@ from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank from .load_reward_table import IDZHandlerLoadRewardTable from .save_topic import IDZHandlerSaveTopic + +from .save_time_attack import IDZHandlerSaveTimeAttack + +from .unlock_profile import IDZHandlerUnlockProfile + +from .update_team_points import IDZHandleUpdateTeamPoints + +from .update_ui_report import IDZHandleUpdateUIReport diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py index fa103af..3ae57f5 100644 --- a/titles/idz/handlers/load_config.py +++ b/titles/idz/handlers/load_config.py @@ -24,7 +24,7 @@ class IDZHandlerLoadConfigA(IDZHandlerBase): return ret class IDZHandlerLoadConfigB(IDZHandlerBase): - cmd_codes = [0x00a0] * IDZConstants.NUM_VERS + cmd_codes = [0x00ab, 0x00ab, 0x00a0, 0x00a0] rsp_codes = [0x00ac, 0x00ac, 0x00a1, 0x00a1] name = "load_config_b" diff --git a/titles/idz/handlers/save_time_attack.py b/titles/idz/handlers/save_time_attack.py new file mode 100644 index 0000000..a28a598 --- /dev/null +++ b/titles/idz/handlers/save_time_attack.py @@ -0,0 +1,22 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerSaveTimeAttack(IDZHandlerBase): + cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136] + rsp_codes = [0x00ce, 0x00ce, 0x00cd, 0x00cd] + name = "save_time_attack" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x00b0 + + if self.version > IDZConstants.VER_IDZ_130: + self.size = 0x00f0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret \ No newline at end of file diff --git a/titles/idz/handlers/unlock_profile.py b/titles/idz/handlers/unlock_profile.py new file mode 100644 index 0000000..a5c8310 --- /dev/null +++ b/titles/idz/handlers/unlock_profile.py @@ -0,0 +1,20 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerUnlockProfile(IDZHandlerBase): + cmd_codes = [0x006f, 0x006f, 0x006b, 0x006b] + rsp_codes = [0x0070, 0x0070, 0x006c, 0x006c] + name = "unlock_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_ui_report.py b/titles/idz/handlers/update_ui_report.py new file mode 100644 index 0000000..b77b4ea --- /dev/null +++ b/titles/idz/handlers/update_ui_report.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandleUpdateUIReport(IDZHandlerBase): + cmd_codes = [0x0084, 0x0084, 0x007e, 0x007e] + name = "update_ui_report" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_user_log.py b/titles/idz/handlers/update_user_log.py new file mode 100644 index 0000000..78679eb --- /dev/null +++ b/titles/idz/handlers/update_user_log.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandleUpdateUserLog(IDZHandlerBase): + cmd_codes = [0x00bd, 0x00bd, 0x00ab, 0x00b3] + name = "update_user_log" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret From 47f4aaddf8742224600ecc99fafe34b3686565ad Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:00:30 -0400 Subject: [PATCH 189/589] allnet: add download order infrastructure --- core/allnet.py | 29 ++++++++++++++++++++++++++--- core/config.py | 5 +++++ index.py | 17 +++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index b7cc4c6..5c10735 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -10,6 +10,7 @@ from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 from time import strptime +from os import path from core.config import CoreConfig from core.utils import Utils @@ -191,12 +192,34 @@ class AllnetServlet: 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: + if not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder: return self.dict_to_http_form_string([vars(resp)]) - else: # TODO: Actual dlorder response + else: # TODO: Keychip check + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-app.ini" + + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): + resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-opt.ini" + + self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) + def handle_dlorder_ini(self, request:Request, match: Dict) -> bytes: + if "file" not in match: return b"" + + req_file = match['file'].replace('%0A', '') + + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): + return open(f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb").read() + + self.logger.info(f"DL INI File {req_file} not found") + return b"" + + def handle_dlorder_report(self, request:Request, match: Dict) -> bytes: + self.logger.info(f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}") + return b"" + def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) request_ip = Utils.get_ip_addr(request) @@ -419,7 +442,7 @@ class AllnetDownloadOrderRequest: class AllnetDownloadOrderResponse: - def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: + def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None: self.stat = stat self.serial = serial self.uri = uri diff --git a/core/config.py b/core/config.py index 9e152c0..84311af 100644 --- a/core/config.py +++ b/core/config.py @@ -188,6 +188,11 @@ class AllnetConfig: self.__config, "core", "allnet", "allow_online_updates", default=False ) + @property + def update_cfg_folder(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "update_cfg_folder", default="" + ) class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: diff --git a/index.py b/index.py index be48ac4..1e1193a 100644 --- a/index.py +++ b/index.py @@ -25,6 +25,22 @@ class HttpDispatcher(resource.Resource): self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir) + + self.map_get.connect( + "allnet_downloadorder_ini", + "/dl/ini/{file}", + controller="allnet", + action="handle_dlorder_ini", + conditions=dict(method=["GET"]), + ) + + self.map_post.connect( + "allnet_downloadorder_report", + "/dl/report", + controller="allnet", + action="handle_dlorder_report", + conditions=dict(method=["POST"]), + ) self.map_post.connect( "allnet_ping", @@ -93,6 +109,7 @@ class HttpDispatcher(resource.Resource): conditions=dict(method=["POST"]), requirements=dict(game=R"S..."), ) + def render_GET(self, request: Request) -> bytes: self.logger.debug(request.uri) From 0668488ccf8bdcc48f4dde782b226887e169868e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:06:44 -0400 Subject: [PATCH 190/589] update core example config --- example_config/core.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/example_config/core.yaml b/example_config/core.yaml index 561293c..382c51b 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -32,6 +32,7 @@ allnet: loglevel: "info" port: 80 allow_online_updates: False + update_cfg_folder: "" billing: port: 8443 From b498e82bf862d57c30ff9e5fab11bceea5b1906e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:08:45 -0400 Subject: [PATCH 191/589] dli: remove dot from version --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 5c10735..07340a2 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -197,10 +197,10 @@ class AllnetServlet: else: # TODO: Keychip check if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-app.ini" + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): - resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-opt.ini" + resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) From f4ee4238d94beea5c0cfddd43bb452dca0aefd40 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 20:51:23 -0400 Subject: [PATCH 192/589] update readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 02f0c67..b1cb506 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + Lily R + Reverse ++ Pokken + + Final Online ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) From 9d23d59e43857e913ac60c834be5d77726fc22f6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 21:04:15 -0400 Subject: [PATCH 193/589] add changelog --- changelog.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e1b5642 --- /dev/null +++ b/changelog.md @@ -0,0 +1,49 @@ +# Changelog +Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. + +## 2023042300 +### Wacca ++ Time free now works properly ++ Fix reverse gate mission causing a fatal error ++ Other misc. fixes ++ Latest DB: 5 + +### Pokken ++ Added preliminary support + + Nothing saves currently, but the game will boot and function properly. + +### Initial D Zero ++ Added preliminary support + + Nothing saves currently, but the game will boot and function for the most part. + +### Mai2 ++ Added support for Festival ++ Lasted DB Version: 4 + +### Ongeki ++ Misc fixes ++ Lasted DB Version: 4 + +### Diva ++ Misc fixes ++ Lasted DB Version: 4 + +### Chuni ++ Fix network encryption ++ Add `handle_remove_token_api_request` for event mode + +### Allnet ++ Added download order support + + It is up to the sysop to provide the INI file, and host the files. + + ONLY for use with cabs. It's not checked currently, which it's why it's default disabled + + YMMV, use at your own risk ++ When running develop mode, games that are not recognised will still be able to authenticate. + +### Database ++ Add autoupgrade command + + Invoke to automatically upgrade all schemas to their latest versions + ++ `version` arg no longer required, leave it blank to update the game schema to latest if it isn't already + +### Misc ++ Update example nginx config file From 238d437519dc0ca621cc1614ab0043134de60ac0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 21:04:52 -0400 Subject: [PATCH 194/589] reformat with black in preperation for merge to master --- core/allnet.py | 50 ++-- core/config.py | 1 + core/data/database.py | 52 ++-- core/data/schema/base.py | 2 +- core/frontend.py | 8 +- core/mucha.py | 12 +- core/utils.py | 12 +- dbutils.py | 19 +- index.py | 3 +- read.py | 3 +- titles/chuni/index.py | 52 ++-- titles/chuni/newplus.py | 8 +- titles/chuni/schema/static.py | 4 +- titles/cm/index.py | 4 +- titles/cm/read.py | 2 +- titles/cxb/index.py | 4 +- titles/idz/config.py | 14 +- titles/idz/const.py | 23 +- titles/idz/database.py | 1 + titles/idz/echo.py | 7 +- titles/idz/handlers/base.py | 7 +- titles/idz/handlers/check_team_names.py | 7 +- titles/idz/handlers/create_auto_team.py | 30 +- titles/idz/handlers/create_profile.py | 48 +-- titles/idz/handlers/discover_profile.py | 7 +- titles/idz/handlers/load_2on2.py | 14 +- titles/idz/handlers/load_config.py | 18 +- titles/idz/handlers/load_ghost.py | 7 +- titles/idz/handlers/load_profile.py | 11 +- titles/idz/handlers/load_reward_table.py | 5 +- titles/idz/handlers/load_server_info.py | 72 +++-- titles/idz/handlers/load_team_ranking.py | 18 +- titles/idz/handlers/load_top_ten.py | 19 +- titles/idz/handlers/lock_profile.py | 19 +- titles/idz/handlers/save_expedition.py | 5 +- titles/idz/handlers/save_profile.py | 3 +- titles/idz/handlers/save_time_attack.py | 11 +- titles/idz/handlers/save_topic.py | 5 +- titles/idz/handlers/unknown.py | 3 +- titles/idz/handlers/unlock_profile.py | 9 +- .../handlers/update_provisional_store_rank.py | 9 +- titles/idz/handlers/update_story_clear_num.py | 11 +- titles/idz/handlers/update_team_points.py | 7 +- titles/idz/handlers/update_ui_report.py | 7 +- titles/idz/handlers/update_user_log.py | 7 +- titles/idz/index.py | 72 +++-- titles/idz/userdb.py | 79 +++-- titles/mai2/base.py | 4 +- titles/mai2/const.py | 2 +- titles/mai2/index.py | 6 +- titles/mai2/schema/item.py | 2 +- titles/mai2/schema/profile.py | 40 ++- titles/ongeki/base.py | 6 +- titles/ongeki/bright.py | 6 +- titles/ongeki/schema/item.py | 4 +- titles/pokken/base.py | 200 +++++++------ titles/pokken/config.py | 1 + titles/pokken/const.py | 1 + titles/pokken/database.py | 1 + titles/pokken/index.py | 33 ++- titles/pokken/schema/item.py | 21 +- titles/pokken/schema/match.py | 38 ++- titles/pokken/schema/profile.py | 278 ++++++++++-------- titles/pokken/schema/static.py | 3 +- titles/wacca/base.py | 48 ++- titles/wacca/handlers/helpers.py | 10 +- titles/wacca/handlers/user_info.py | 2 +- titles/wacca/handlers/user_status.py | 6 +- titles/wacca/lily.py | 18 +- titles/wacca/reverse.py | 2 +- 70 files changed, 920 insertions(+), 603 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 07340a2..119f0ae 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -105,9 +105,11 @@ class AllnetServlet: resp.stat = 0 return self.dict_to_http_form_string([vars(resp)]) - + else: - self.logger.info(f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}") + self.logger.info( + f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" + ) resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" return self.dict_to_http_form_string([vars(resp)]) @@ -189,35 +191,49 @@ 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}") + 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 or not self.config.allnet.update_cfg_folder: + + if ( + not self.config.allnet.allow_online_updates + or not self.config.allnet.update_cfg_folder + ): return self.dict_to_http_form_string([vars(resp)]) else: # TODO: Keychip check - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" + ): resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" - - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): + + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" + ): resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" - + self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) - def handle_dlorder_ini(self, request:Request, match: Dict) -> bytes: - if "file" not in match: return b"" + def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: + if "file" not in match: + return b"" + + req_file = match["file"].replace("%0A", "") - req_file = match['file'].replace('%0A', '') - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): - return open(f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb").read() - + return open( + f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" + ).read() + self.logger.info(f"DL INI File {req_file} not found") return b"" - def handle_dlorder_report(self, request:Request, match: Dict) -> bytes: - self.logger.info(f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}") + def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: + self.logger.info( + f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}" + ) return b"" def handle_billing_request(self, request: Request, _: Dict): diff --git a/core/config.py b/core/config.py index 84311af..3fb0dbe 100644 --- a/core/config.py +++ b/core/config.py @@ -194,6 +194,7 @@ class AllnetConfig: self.__config, "core", "allnet", "update_cfg_folder", default="" ) + class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config diff --git a/core/data/database.py b/core/data/database.py index 40bbf33..719d05e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -71,7 +71,9 @@ class Data: games = Utils.get_all_titles() for game_dir, game_mod in games.items(): try: - if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"): + if hasattr(game_mod, "database") and hasattr( + game_mod, "current_schema_version" + ): game_mod.database(self.config) metadata.create_all(self.__engine.connect()) @@ -135,21 +137,26 @@ class Data: if version is None: if not game == "CORE": titles = Utils.get_all_titles() - + for folder, mod in titles.items(): - if not mod.game_codes[0] == game: continue - + if not mod.game_codes[0] == game: + continue + if hasattr(mod, "current_schema_version"): version = mod.current_schema_version - + else: - self.logger.warn(f"current_schema_version not found for {folder}") - + self.logger.warn( + f"current_schema_version not found for {folder}" + ) + else: version = self.current_schema_version - + if version is None: - self.logger.warn(f"Could not determine latest version for {game}, please specify --version") + self.logger.warn( + f"Could not determine latest version for {game}, please specify --version" + ) if old_ver is None: self.logger.error( @@ -184,7 +191,7 @@ class Data: 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( @@ -285,13 +292,13 @@ class Data: if all_game_versions is None: self.logger.warn("Failed to get schema versions") return - + all_games = Utils.get_all_titles() all_games_list: Dict[str, int] = {} for _, mod in all_games.items(): if hasattr(mod, "current_schema_version"): all_games_list[mod.game_codes[0]] = mod.current_schema_version - + for x in all_game_versions: failed = False game = x["game"].upper() @@ -299,27 +306,30 @@ class Data: latest_ver = all_games_list.get(game, 1) if game == "CORE": latest_ver = self.current_schema_version - - if update_ver == latest_ver: + + if update_ver == latest_ver: self.logger.info(f"{game} is already latest version") continue - + for y in range(update_ver + 1, latest_ver + 1): if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): with open( - f"core/data/schema/versions/{game}_{y}_upgrade.sql", - "r", - encoding="utf-8", + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", ) as f: sql = f.read() result = self.base.execute(sql) if result is None: - self.logger.error(f"Error execuing sql script for game {game} v{y}!") + self.logger.error( + f"Error execuing sql script for game {game} v{y}!" + ) failed = True break else: self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") failed = True - - if not failed: self.base.set_schema_ver(latest_ver, game) + + if not failed: + self.base.set_schema_ver(latest_ver, game) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index ea2fc88..7957301 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -81,7 +81,7 @@ 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) diff --git a/core/frontend.py b/core/frontend.py index 0c96fdc..c992e76 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -66,14 +66,16 @@ class FrontendServlet(resource.Resource): fe_game = FE_Game(cfg, self.environment) games = Utils.get_all_titles() for game_dir, game_mod in games.items(): - if hasattr(game_mod, "frontend"): + if hasattr(game_mod, "frontend"): try: game_fe = game_mod.frontend(cfg, self.environment, config_dir) self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) fe_game.putChild(game_dir.encode(), game_fe) - + except Exception as e: - self.logger.error(f"Failed to import frontend from {game_dir} because {e}") + self.logger.error( + f"Failed to import frontend from {game_dir} because {e}" + ) self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) diff --git a/core/mucha.py b/core/mucha.py index 9dfef03..a90ab53 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -46,9 +46,7 @@ class MuchaServlet: if enabled: self.mucha_registry.append(game_cd) - self.logger.info( - f"Serving {len(self.mucha_registry)} games" - ) + self.logger.info(f"Serving {len(self.mucha_registry)} games") def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) @@ -62,9 +60,7 @@ class MuchaServlet: req = MuchaAuthRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info( - f"Boardauth request from {client_ip} for {req.gameVer}" - ) + self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") @@ -92,9 +88,7 @@ class MuchaServlet: req = MuchaUpdateRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info( - f"Updatecheck request from {client_ip} for {req.gameVer}" - ) + self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") diff --git a/core/utils.py b/core/utils.py index 6bd5c08..f364785 100644 --- a/core/utils.py +++ b/core/utils.py @@ -16,14 +16,20 @@ class Utils: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - if hasattr(mod, "game_codes") and hasattr(mod, "index"): # Minimum required to function + if hasattr(mod, "game_codes") and hasattr( + mod, "index" + ): # Minimum required to function ret[dir] = mod except ImportError as e: logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") raise return ret - + @classmethod def get_ip_addr(cls, req: Request) -> str: - return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host + return ( + req.getAllHeaders()[b"x-forwarded-for"].decode() + if b"x-forwarded-for" in req.getAllHeaders() + else req.getClientAddress().host + ) diff --git a/dbutils.py b/dbutils.py index 3be3ebe..d959232 100644 --- a/dbutils.py +++ b/dbutils.py @@ -34,12 +34,12 @@ if __name__ == "__main__": cfg = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) - cfg_dict.get('database', {})['loglevel'] = 'info' + cfg_dict.get("database", {})["loglevel"] = "info" cfg.update(cfg_dict) - + if not path.exists(cfg.server.log_dir): mkdir(cfg.server.log_dir) - + if not access(cfg.server.log_dir, W_OK): print( f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" @@ -47,7 +47,6 @@ if __name__ == "__main__": exit(1) data = Data(cfg) - if args.action == "create": data.create_database() @@ -61,10 +60,18 @@ if __name__ == "__main__": if args.game is None: data.logger.warn("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version) if args.version is not None else None, args.action) + data.migrate_database( + "CORE", + int(args.version) if args.version is not None else None, + args.action, + ) else: - data.migrate_database(args.game, int(args.version) if args.version is not None else None, args.action) + data.migrate_database( + args.game, + int(args.version) if args.version is not None else None, + args.action, + ) elif args.action == "autoupgrade": data.autoupgrade() diff --git a/index.py b/index.py index 1e1193a..11fad94 100644 --- a/index.py +++ b/index.py @@ -25,7 +25,7 @@ class HttpDispatcher(resource.Resource): self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir) - + self.map_get.connect( "allnet_downloadorder_ini", "/dl/ini/{file}", @@ -109,7 +109,6 @@ class HttpDispatcher(resource.Resource): conditions=dict(method=["POST"]), requirements=dict(game=R"S..."), ) - def render_GET(self, request: Request) -> bytes: self.logger.debug(request.uri) diff --git a/read.py b/read.py index a1bd0ab..14c5cc2 100644 --- a/read.py +++ b/read.py @@ -135,8 +135,7 @@ 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/titles/chuni/index.py b/titles/chuni/index.py index 53db19f..a7545ba 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -82,21 +82,33 @@ 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('__')] + + 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) - + 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.debug( + f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" + ) @classmethod def get_allnet_info( @@ -164,18 +176,22 @@ class ChuniServlet: # technically not 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") + 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}") + self.logger.error( + f"No hash found for v{version} endpoint {endpoint}" + ) return zlib.compress(b'{"stat": "0"}') endpoint = self.hash_table[internal_ver][endpoint.lower()] - + try: crypt = AES.new( bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), @@ -193,7 +209,11 @@ class ChuniServlet: encrtped = True - if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: + 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}" ) @@ -210,9 +230,7 @@ class ChuniServlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 422d57a..4faf47a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -13,8 +13,12 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] - ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[ + "rom" + ] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[ + "data" + ] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index bef58c0..4537518 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -200,7 +200,9 @@ class ChuniStaticData(BaseData): return result.lastrowid def get_login_bonus( - self, version: int, preset_id: int, + self, + version: int, + preset_id: int, ) -> Optional[List[Row]]: sql = login_bonus.select( and_( diff --git a/titles/cm/index.py b/titles/cm/index.py index 3a56566..74d3a0d 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -109,9 +109,7 @@ class CardMakerServlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/cm/read.py b/titles/cm/read.py index 93ff203..109483c 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -90,7 +90,7 @@ class CardMakerReader(BaseReader): "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 + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1, } for root, dirs, files in os.walk(base_dir): diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 36c762e..0c38d55 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -101,9 +101,7 @@ class CxbServlet(resource.Resource): f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}" ) else: - self.logger.info( - f"Ready on port {self.game_cfg.server.port}" - ) + self.logger.info(f"Ready on port {self.game_cfg.server.port}") def render_POST(self, request: Request): version = 0 diff --git a/titles/idz/config.py b/titles/idz/config.py index 2ff4e5c..f7af4fd 100644 --- a/titles/idz/config.py +++ b/titles/idz/config.py @@ -46,15 +46,21 @@ class IDZPortsConfig: @property def userdb(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "userdb", default=10000) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "userdb", default=10000 + ) @property def match(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "match", default=10010) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "match", default=10010 + ) @property def echo(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "echo", default=10020) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "echo", default=10020 + ) class IDZConfig(dict): @@ -64,4 +70,4 @@ class IDZConfig(dict): @property def rsa_keys(self) -> List[Dict]: - return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) \ No newline at end of file + return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) diff --git a/titles/idz/const.py b/titles/idz/const.py index 164c940..93cbb4c 100644 --- a/titles/idz/const.py +++ b/titles/idz/const.py @@ -1,5 +1,6 @@ from enum import Enum + class IDZConstants: GAME_CODE = "SDDF" @@ -22,14 +23,26 @@ class IDZConstants: LOCKED = 0 UNLOCKED = 1 OLD = 2 - + HASH_LUT = [ # No clue - 0x9C82E674, 0x5A4738D9, 0x8B8D7AE0, 0x29EC9D81, + 0x9C82E674, + 0x5A4738D9, + 0x8B8D7AE0, + 0x29EC9D81, # These three are from AES TE0 - 0x1209091B, 0x1D83839E, 0x582C2C74, 0x341A1A2E, - 0x361B1B2D, 0xDC6E6EB2, 0xB45A5AEE, 0x5BA0A0FB, - 0xA45252F6, 0x763B3B4D, 0xB7D6D661, 0x7DB3B3CE, + 0x1209091B, + 0x1D83839E, + 0x582C2C74, + 0x341A1A2E, + 0x361B1B2D, + 0xDC6E6EB2, + 0xB45A5AEE, + 0x5BA0A0FB, + 0xA45252F6, + 0x763B3B4D, + 0xB7D6D661, + 0x7DB3B3CE, ] HASH_NUM = 0 HASH_MUL = [5, 7, 11, 12][HASH_NUM] diff --git a/titles/idz/database.py b/titles/idz/database.py index 1f5a8e2..525b1c1 100644 --- a/titles/idz/database.py +++ b/titles/idz/database.py @@ -1,6 +1,7 @@ from core.data import Data from core.config import CoreConfig + class IDZData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) diff --git a/titles/idz/echo.py b/titles/idz/echo.py index 4414b70..979fd19 100644 --- a/titles/idz/echo.py +++ b/titles/idz/echo.py @@ -4,6 +4,7 @@ import logging from core.config import CoreConfig from .config import IDZConfig + class IDZEcho(DatagramProtocol): def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: super().__init__() @@ -12,5 +13,7 @@ class IDZEcho(DatagramProtocol): self.logger = logging.getLogger("idz") def datagramReceived(self, data, addr): - self.logger.debug(f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}") - self.transport.write(data, addr) \ No newline at end of file + self.logger.debug( + f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" + ) + self.transport.write(data, addr) diff --git a/titles/idz/handlers/base.py b/titles/idz/handlers/base.py index 720f097..6b1e5d5 100644 --- a/titles/idz/handlers/base.py +++ b/titles/idz/handlers/base.py @@ -5,7 +5,8 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants -class IDZHandlerBase(): + +class IDZHandlerBase: name = "generic" cmd_codes = [0x0000] * IDZConstants.NUM_VERS rsp_codes = [0x0001] * IDZConstants.NUM_VERS @@ -18,8 +19,8 @@ class IDZHandlerBase(): self.game = IDZConstants.GAME_CODE self.version = version self.size = 0x30 - + def handle(self, data: bytes) -> bytearray: ret = bytearray([0] * self.size) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ca0 - + self.size = 0x0CA0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0020 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" 2: struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) user_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04c0 + self.size = 0x04C0 if version >= IDZConstants.VER_IDZ_210: self.size = 0x1290 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) + class IDZHandlerLoad2on2B(IDZHandlerBase): cmd_codes = [0x0132] * 4 rsp_codes = [0x0133] * 4 @@ -27,10 +29,10 @@ class IDZHandlerLoad2on2B(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04c0 + self.size = 0x04C0 if version >= IDZConstants.VER_IDZ_210: self.size = 0x0540 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py index 3ae57f5..b3ceb0d 100644 --- a/titles/idz/handlers/load_config.py +++ b/titles/idz/handlers/load_config.py @@ -5,6 +5,7 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerLoadConfigA(IDZHandlerBase): cmd_codes = [0x0004] * IDZConstants.NUM_VERS rsp_codes = [0x0005] * IDZConstants.NUM_VERS @@ -12,29 +13,30 @@ class IDZHandlerLoadConfigA(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x01a0 - + self.size = 0x01A0 + if self.version > 1: - self.size = 0x05e0 - + self.size = 0x05E0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0230 - + if self.version > 1: self.size = 0x0240 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0070 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) if self.version == IDZConstants.VER_IDZ_110: - self.size = 0x0d30 + self.size = 0x0D30 elif self.version == IDZConstants.VER_IDZ_130: - self.size = 0x0ea0 + self.size = 0x0EA0 elif self.version == IDZConstants.VER_IDZ_210: self.size = 0x1360 elif self.version == IDZConstants.VER_IDZ_230: self.size = 0x1640 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x01c0 - + self.size = 0x01C0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index 3b60f00..ef6e81c 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -5,6 +5,7 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerLoadServerInfo(IDZHandlerBase): cmd_codes = [0x0006] * IDZConstants.NUM_VERS rsp_codes = [0x0007] * IDZConstants.NUM_VERS @@ -12,14 +13,14 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04b0 - + self.size = 0x04B0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) offset = 0 - if self.version >= IDZConstants.VER_IDZ_210: + if self.version >= IDZConstants.VER_IDZ_210: offset = 2 - + news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" @@ -27,34 +28,69 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): len_news = len(news_str) len_error = len(err_str) - struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ba0 - + self.size = 0x0BA0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) + class IDZHandlerLoadTeamRankingB(IDZHandlerBase): - cmd_codes = [0x00bb, 0x00bb, 0x00a9, 0x00a9] - rsp_codes = [0x00a8] * 4 + cmd_codes = [0x00BB, 0x00BB, 0x00A9, 0x00A9] + rsp_codes = [0x00A8] * 4 name = "load_team_ranking_b" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ba0 - + self.size = 0x0BA0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_top_ten.py b/titles/idz/handlers/load_top_ten.py index ef2a158..09a9f5f 100644 --- a/titles/idz/handlers/load_top_ten.py +++ b/titles/idz/handlers/load_top_ten.py @@ -5,9 +5,10 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerLoadTopTen(IDZHandlerBase): - cmd_codes = [0x012c] * 4 - rsp_codes = [0x00ce] * 4 + cmd_codes = [0x012C] * 4 + rsp_codes = [0x00CE] * 4 name = "load_top_ten" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: @@ -19,12 +20,16 @@ class IDZHandlerLoadTopTen(IDZHandlerBase): tracks_dates: List[Tuple[int, int]] = [] for i in range(32): tracks_dates.append( - (struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0020 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) profile_data = { - "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, - "expire_time": int((datetime.now() + timedelta(hours=1)).timestamp() / 1000) + "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, + "expire_time": int( + (datetime.now() + timedelta(hours=1)).timestamp() / 1000 + ), } user_id = struct.unpack_from(" bytearray: struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/save_profile.py b/titles/idz/handlers/save_profile.py index 652dbd6..3f5311d 100644 --- a/titles/idz/handlers/save_profile.py +++ b/titles/idz/handlers/save_profile.py @@ -4,12 +4,13 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerSaveProfile(IDZHandlerBase): cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143] name = "save_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/save_time_attack.py b/titles/idz/handlers/save_time_attack.py index a28a598..bea83af 100644 --- a/titles/idz/handlers/save_time_attack.py +++ b/titles/idz/handlers/save_time_attack.py @@ -5,18 +5,19 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerSaveTimeAttack(IDZHandlerBase): cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136] - rsp_codes = [0x00ce, 0x00ce, 0x00cd, 0x00cd] + rsp_codes = [0x00CE, 0x00CE, 0x00CD, 0x00CD] name = "save_time_attack" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x00b0 + self.size = 0x00B0 if self.version > IDZConstants.VER_IDZ_130: - self.size = 0x00f0 - + self.size = 0x00F0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) - return ret \ No newline at end of file + return ret diff --git a/titles/idz/handlers/save_topic.py b/titles/idz/handlers/save_topic.py index 9499385..090ce52 100644 --- a/titles/idz/handlers/save_topic.py +++ b/titles/idz/handlers/save_topic.py @@ -4,6 +4,7 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerSaveTopic(IDZHandlerBase): cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091] rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092] @@ -11,7 +12,7 @@ class IDZHandlerSaveTopic(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x05d0 - + self.size = 0x05D0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/unknown.py b/titles/idz/handlers/unknown.py index ee3046a..8998d81 100644 --- a/titles/idz/handlers/unknown.py +++ b/titles/idz/handlers/unknown.py @@ -4,8 +4,9 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerUnknown(IDZHandlerBase): - cmd_codes = [0x00ad, 0x00ad, 0x00a2, 0x00a2] + cmd_codes = [0x00AD, 0x00AD, 0x00A2, 0x00A2] name = "unknown" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: diff --git a/titles/idz/handlers/unlock_profile.py b/titles/idz/handlers/unlock_profile.py index a5c8310..1be50f5 100644 --- a/titles/idz/handlers/unlock_profile.py +++ b/titles/idz/handlers/unlock_profile.py @@ -5,16 +5,17 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerUnlockProfile(IDZHandlerBase): - cmd_codes = [0x006f, 0x006f, 0x006b, 0x006b] - rsp_codes = [0x0070, 0x0070, 0x006c, 0x006c] + cmd_codes = [0x006F, 0x006F, 0x006B, 0x006B] + rsp_codes = [0x0070, 0x0070, 0x006C, 0x006C] name = "unlock_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x02b0 - + self.size = 0x02B0 + def handle(self, data: bytes) -> bytearray: - return super().handle(data) - + return super().handle(data) + def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray: pass diff --git a/titles/idz/handlers/update_story_clear_num.py b/titles/idz/handlers/update_story_clear_num.py index 46f2689..bcf44a5 100644 --- a/titles/idz/handlers/update_story_clear_num.py +++ b/titles/idz/handlers/update_story_clear_num.py @@ -5,22 +5,23 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerUpdateStoryClearNum(IDZHandlerBase): - cmd_codes = [0x007f, 0x097f, 0x013d, 0x0144] - rsp_codes = [0x0080, 0x013e, 0x013e, 0x0145] + cmd_codes = [0x007F, 0x097F, 0x013D, 0x0144] + rsp_codes = [0x0080, 0x013E, 0x013E, 0x0145] name = "update_story_clear_num" - + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) if self.version == IDZConstants.VER_IDZ_110: self.size = 0x0220 elif self.version == IDZConstants.VER_IDZ_130: - self.size = 0x04f0 + self.size = 0x04F0 elif self.version == IDZConstants.VER_IDZ_210: self.size = 0x0510 elif self.version == IDZConstants.VER_IDZ_230: self.size = 0x0800 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/update_team_points.py b/titles/idz/handlers/update_team_points.py index c1f69ca..a23d843 100644 --- a/titles/idz/handlers/update_team_points.py +++ b/titles/idz/handlers/update_team_points.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateTeamPoints(IDZHandlerBase): - cmd_codes = [0x0081, 0x0081, 0x007b, 0x007b] + cmd_codes = [0x0081, 0x0081, 0x007B, 0x007B] name = "unlock_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/handlers/update_ui_report.py b/titles/idz/handlers/update_ui_report.py index b77b4ea..7e99b40 100644 --- a/titles/idz/handlers/update_ui_report.py +++ b/titles/idz/handlers/update_ui_report.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateUIReport(IDZHandlerBase): - cmd_codes = [0x0084, 0x0084, 0x007e, 0x007e] + cmd_codes = [0x0084, 0x0084, 0x007E, 0x007E] name = "update_ui_report" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/handlers/update_user_log.py b/titles/idz/handlers/update_user_log.py index 78679eb..c862f52 100644 --- a/titles/idz/handlers/update_user_log.py +++ b/titles/idz/handlers/update_user_log.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateUserLog(IDZHandlerBase): - cmd_codes = [0x00bd, 0x00bd, 0x00ab, 0x00b3] + cmd_codes = [0x00BD, 0x00BD, 0x00AB, 0x00B3] name = "update_user_log" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/index.py b/titles/idz/index.py index 5b60e5a..0f26a30 100644 --- a/titles/idz/index.py +++ b/titles/idz/index.py @@ -16,6 +16,7 @@ from .userdb import IDZUserDBFactory, IDZUserDBWeb, IDZKey from .echo import IDZEcho from .handlers import IDZHandlerLoadConfigB + class IDZServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg @@ -51,13 +52,16 @@ class IDZServlet: level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) self.logger.inited = True - + @classmethod def rsaHashKeyN(cls, data): hash_ = 0 for i in data: - hash_ = hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) ^ IDZConstants.HASH_LUT[i & 0xf] - hash_ &= 0xffffffff + hash_ = ( + hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) + ^ IDZConstants.HASH_LUT[i & 0xF] + ) + hash_ &= 0xFFFFFFFF return hash_ @classmethod @@ -77,14 +81,18 @@ class IDZServlet: logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start") return (False, "", "") - hostname = core_cfg.title.hostname if not game_cfg.server.hostname else game_cfg.server.hostname + hostname = ( + core_cfg.title.hostname + if not game_cfg.server.hostname + else game_cfg.server.hostname + ) return ( True, f"", f"{hostname}:{game_cfg.ports.userdb}", ) - def setup(self): + def setup(self): for key in self.game_cfg.rsa_keys: if "N" not in key or "d" not in key or "e" not in key: self.logger.error(f"Invalid IDZ key {key}") @@ -92,14 +100,14 @@ class IDZServlet: hashN = self.rsaHashKeyN(str(key["N"]).encode()) self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN)) - + if len(self.rsa_keys) <= 0: self.logger.error("No valid RSA keys provided! IDZ cannot start!") return handler_map = [{} for _ in range(IDZConstants.NUM_VERS)] handler_mod = mod = importlib.import_module(f"titles.idz.handlers") - + for cls_name in dir(handler_mod): if cls_name.startswith("__"): continue @@ -109,7 +117,7 @@ class IDZServlet: mod_cmds: List = getattr(mod, "cmd_codes") while len(mod_cmds) < IDZConstants.NUM_VERS: mod_cmds.append(None) - + for i in range(len(mod_cmds)): if mod_cmds[i] is None: mod_cmds[i] = mod_cmds[i - 1] @@ -119,27 +127,47 @@ class IDZServlet: except AttributeError as e: continue - endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}")\ - .listen(IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map)) - - reactor.listenUDP(self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg)) - + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}", + ).listen( + IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map) + ) + + reactor.listenUDP( + self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - req_raw = request.content.getvalue() + req_raw = request.content.getvalue() self.logger.info(f"IDZ POST request: {url_path} - {req_raw}") return b"" def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - self.logger.info(f"IDZ GET request: {url_path}") - request.responseHeaders.setRawHeaders('Content-Type', [b"text/plain; charset=utf-8"]) - request.responseHeaders.setRawHeaders("Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"]) - - news = self.game_cfg.server.news if self.game_cfg.server.news else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + self.logger.info(f"IDZ GET request: {url_path}") + request.responseHeaders.setRawHeaders( + "Content-Type", [b"text/plain; charset=utf-8"] + ) + request.responseHeaders.setRawHeaders( + "Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"] + ) + + news = ( + self.game_cfg.server.news + if self.game_cfg.server.news + else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + ) news += "\r\n" news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index 95a2eb9..2f70ba4 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -19,6 +19,7 @@ from .handlers import IDZHandlerBase HANDLER_MAP: List[Dict] + class IDZKey: def __init__(self, n, d, e, hashN: int) -> None: self.N = n @@ -26,9 +27,16 @@ class IDZKey: self.e = e self.hashN = hashN + class IDZUserDBProtocol(Protocol): - def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, keys: List[IDZKey], handlers: List[Dict]) -> None: - self.logger = logging.getLogger('idz') + def __init__( + self, + core_cfg: CoreConfig, + game_cfg: IDZConfig, + keys: List[IDZKey], + handlers: List[Dict], + ) -> None: + self.logger = logging.getLogger("idz") self.core_config = core_cfg self.game_config = game_cfg self.rsa_keys = keys @@ -37,14 +45,14 @@ class IDZUserDBProtocol(Protocol): self.version = None self.version_internal = None self.skip_next = False - + def append_padding(self, data: bytes): """Appends 0s to the end of the data until it's at the correct size""" length = struct.unpack_from(" None: self.logger.debug(f"{self.transport.getPeer().host} Connected") base = 0 @@ -57,13 +65,17 @@ class IDZUserDBProtocol(Protocol): rsa_key = random.choice(self.rsa_keys) key_enc: int = pow(base, rsa_key.e, rsa_key.N) - result = key_enc.to_bytes(0x40, "little") + struct.pack(" None: + def connectionLost(self, reason) -> None: self.logger.debug( f"{self.transport.getPeer().host} Disconnected - {reason.value}" ) @@ -84,7 +96,7 @@ class IDZUserDBProtocol(Protocol): self.transport.write(b"\x00") return - elif magic == 0x01020304: + elif magic == 0x01020304: self.version = int(data_dec[16:19].decode()) if self.version == 110: @@ -99,10 +111,12 @@ class IDZUserDBProtocol(Protocol): self.logger.warn(f"Bad version v{self.version}") self.version = None self.version_internal = None - - self.logger.debug(f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}") + + self.logger.debug( + f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}" + ) return - + elif self.skip_next: self.skip_next = False self.transport.write(b"\x00") @@ -110,19 +124,25 @@ class IDZUserDBProtocol(Protocol): elif self.version is None: # We didn't get a handshake before, and this isn't one now, so we're up the creek - self.logger.info(f"Bad UserDB request from from {self.transport.getPeer().host}") + self.logger.info( + f"Bad UserDB request from from {self.transport.getPeer().host}" + ) self.transport.write(b"\x00") return cmd = struct.unpack_from(" None: + def __init__( + self, + cfg: CoreConfig, + game_cfg: IDZConfig, + keys: List[IDZKey], + handlers: List[Dict], + ) -> None: self.core_config = cfg self.game_config = game_cfg self.keys = keys self.handlers = handlers def buildProtocol(self, addr): - return IDZUserDBProtocol(self.core_config, self.game_config, self.keys, self.handlers) + return IDZUserDBProtocol( + self.core_config, self.game_config, self.keys, self.handlers + ) + class IDZUserDBWeb(resource.Resource): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): @@ -151,12 +180,16 @@ class IDZUserDBWeb(resource.Resource): self.isLeaf = True self.core_config = core_cfg self.game_config = game_cfg - self.logger = logging.getLogger('idz') - + self.logger = logging.getLogger("idz") + def render_POST(self, request: Request) -> bytes: - self.logger.info(f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}") + self.logger.info( + f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}" + ) return b"" def render_GET(self, request: Request) -> bytes: - self.logger.info(f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}") - return b"" \ No newline at end of file + self.logger.info( + f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}" + ) + return b"" diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 9f5c8af..171378c 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -300,7 +300,9 @@ class Mai2Base: ): for fsr in upsert["userFriendSeasonRankingList"]: fsr["recordDate"] = ( - datetime.strptime(fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"), + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), ) self.data.item.put_friend_season_ranking(user_id, fsr) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 9694aba..dcc7e29 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -39,7 +39,7 @@ class Mai2Constants: "maimai DX Splash PLUS", "maimai DX Universe", "maimai DX Universe PLUS", - "maimai DX Festival" + "maimai DX Festival", ) @classmethod diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 3618f4d..1b92842 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -38,7 +38,7 @@ class Mai2Servlet: Mai2SplashPlus, Mai2Universe, Mai2UniversePlus, - Mai2Festival + Mai2Festival, ] self.logger = logging.getLogger("mai2") @@ -134,9 +134,7 @@ class Mai2Servlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 980623e..6280bbb 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -402,7 +402,7 @@ class Mai2ItemData(BaseData): if result is None: self.logger.warn( f"put_friend_season_ranking: failed to insert", - f"friend_season_ranking! aime_id: {aime_id}" + f"friend_season_ranking! aime_id: {aime_id}", ) return None return result.lastrowid diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index b3802e5..3cb42d1 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -299,9 +299,11 @@ class Mai2ProfileData(BaseData): 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) - ).order_by(detail.c.version.desc()) + sql = ( + select(detail) + .where(and_(detail.c.user == user_id, detail.c.version <= version)) + .order_by(detail.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -324,9 +326,11 @@ 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) - ).order_by(ghost.c.version.desc()) + sql = ( + select(ghost) + .where(and_(ghost.c.user == user_id, ghost.c.version_int <= version)) + .order_by(ghost.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -349,9 +353,11 @@ 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) - ).order_by(extend.c.version.desc()) + sql = ( + select(extend) + .where(and_(extend.c.user == user_id, extend.c.version <= version)) + .order_by(extend.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -374,9 +380,11 @@ 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) - ).order_by(option.c.version.desc()) + sql = ( + select(option) + .where(and_(option.c.user == user_id, option.c.version <= version)) + .order_by(option.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -399,9 +407,11 @@ 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) - ).order_by(rating.c.version.desc()) + sql = ( + select(rating) + .where(and_(rating.c.user == user_id, rating.c.version <= version)) + .order_by(rating.c.version.desc()) + ) result = self.execute(sql) if result is None: diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 10bb1a8..4f7619c 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -452,8 +452,7 @@ 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 @@ -852,8 +851,7 @@ 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 23eeb6c..06155a1 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -97,7 +97,7 @@ class OngekiBright(OngekiBase): "userId": data["userId"], "length": 0, "nextIndex": 0, - "userCharacterList": [] + "userCharacterList": [], } max_ct = data["maxCount"] @@ -548,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: @@ -556,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/schema/item.py b/titles/ongeki/schema/item.py index d826fba..27d90f8 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -705,9 +705,7 @@ class OngekiItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 0e87397..40e6444 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -74,27 +74,21 @@ class PokkenBase: return res.SerializeToString() - def handle_save_client_log( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG return res.SerializeToString() - def handle_check_diagnosis( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS return res.SerializeToString() - def handle_load_client_settings( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS @@ -129,26 +123,28 @@ class PokkenBase: ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) return res.SerializeToString() - + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_USER - access_code = request.load_user.access_code + access_code = request.load_user.access_code load_usr = jackal_pb2.LoadUserResponseData() user_id = self.data.card.get_user_id_from_card(access_code) if user_id is None and self.game_cfg.server.auto_register: user_id = self.data.user.create_user() card_id = self.data.card.create_card(user_id, access_code) - - self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") - + + self.logger.info( + f"Register new card {access_code} (UserId {user_id}, CardId {card_id})" + ) + elif user_id is None: self.logger.info(f"Registration of card {access_code} blocked!") res.load_user.CopyFrom(load_usr) return res.SerializeToString() - + """ TODO: Add repeated values tutorial_progress_flag @@ -168,12 +164,12 @@ class PokkenBase: load_usr.load_hash = 1 load_usr.cardlock_status = False load_usr.banapass_id = user_id - load_usr.access_code = access_code + load_usr.access_code = access_code load_usr.precedent_release_flag = 0xFFFFFFFF - + if profile is None: - profile_id = self.data.profile.create_profile(user_id) - profile_dict = {'id': profile_id, 'user': user_id} + profile_id = self.data.profile.create_profile(user_id) + profile_dict = {"id": profile_id, "user": user_id} pokemon_data = [] tutorial_progress = [] rankmatch_progress = [] @@ -181,10 +177,12 @@ class PokkenBase: event_achievement_flag = [] event_achievement_param = [] load_usr.new_card_flag = True - + else: - profile_dict = { k: v for k, v in profile._asdict().items() if v is not None } - self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})") + profile_dict = {k: v for k, v in profile._asdict().items() if v is not None} + self.logger.info( + f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})" + ) pokemon_data = self.data.profile.get_all_pokemon_data(user_id) tutorial_progress = [] rankmatch_progress = [] @@ -193,76 +191,78 @@ class PokkenBase: event_achievement_param = [] load_usr.new_card_flag = False - load_usr.navi_newbie_flag = profile_dict.get('navi_newbie_flag', True) - load_usr.navi_enable_flag = profile_dict.get('navi_enable_flag', True) - load_usr.pad_vibrate_flag = profile_dict.get('pad_vibrate_flag', True) - load_usr.home_region_code = profile_dict.get('home_region_code', 0) - load_usr.home_loc_name = profile_dict.get('home_loc_name', "") - load_usr.pref_code = profile_dict.get('pref_code', 0) - load_usr.trainer_name = profile_dict.get('trainer_name', "Newb" + str(random.randint(1111,999999))) - load_usr.trainer_rank_point = profile_dict.get('trainer_rank_point', 0) - load_usr.wallet = profile_dict.get('wallet', 0) - load_usr.fight_money = profile_dict.get('fight_money', 0) - load_usr.score_point = profile_dict.get('score_point', 0) - load_usr.grade_max_num = profile_dict.get('grade_max_num', 0) - load_usr.extra_counter = profile_dict.get('extra_counter', 0) - load_usr.total_play_days = profile_dict.get('total_play_days', 0) - load_usr.play_date_time = profile_dict.get('play_date_time', 0) - load_usr.lucky_box_fail_num = profile_dict.get('lucky_box_fail_num', 0) - load_usr.event_reward_get_flag = profile_dict.get('event_reward_get_flag', 0) - load_usr.rank_pvp_all = profile_dict.get('rank_pvp_all', 0) - load_usr.rank_pvp_loc = profile_dict.get('rank_pvp_loc', 0) - load_usr.rank_cpu_all = profile_dict.get('rank_cpu_all', 0) - load_usr.rank_cpu_loc = profile_dict.get('rank_cpu_loc', 0) - load_usr.rank_event = profile_dict.get('rank_event', 0) - load_usr.awake_num = profile_dict.get('awake_num', 0) - load_usr.use_support_num = profile_dict.get('use_support_num', 0) - load_usr.rankmatch_flag = profile_dict.get('rankmatch_flag', 0) - load_usr.rankmatch_max = profile_dict.get('rankmatch_max', 0) - load_usr.rankmatch_success = profile_dict.get('rankmatch_success', 0) - load_usr.beat_num = profile_dict.get('beat_num', 0) - load_usr.title_text_id = profile_dict.get('title_text_id', 0) - load_usr.title_plate_id = profile_dict.get('title_plate_id', 0) - load_usr.title_decoration_id = profile_dict.get('title_decoration_id', 0) - load_usr.navi_trainer = profile_dict.get('navi_trainer', 0) - load_usr.navi_version_id = profile_dict.get('navi_version_id', 0) - load_usr.aid_skill = profile_dict.get('aid_skill', 0) - load_usr.comment_text_id = profile_dict.get('comment_text_id', 0) - load_usr.comment_word_id = profile_dict.get('comment_word_id', 0) - load_usr.latest_use_pokemon = profile_dict.get('latest_use_pokemon', 0) - load_usr.ex_ko_num = profile_dict.get('ex_ko_num', 0) - load_usr.wko_num = profile_dict.get('wko_num', 0) - load_usr.timeup_win_num = profile_dict.get('timeup_win_num', 0) - load_usr.cool_ko_num = profile_dict.get('cool_ko_num', 0) - load_usr.perfect_ko_num = profile_dict.get('perfect_ko_num', 0) - load_usr.record_flag = profile_dict.get('record_flag', 0) - load_usr.site_register_status = profile_dict.get('site_register_status', 0) - load_usr.continue_num = profile_dict.get('continue_num', 0) + load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True) + load_usr.navi_enable_flag = profile_dict.get("navi_enable_flag", True) + load_usr.pad_vibrate_flag = profile_dict.get("pad_vibrate_flag", True) + load_usr.home_region_code = profile_dict.get("home_region_code", 0) + load_usr.home_loc_name = profile_dict.get("home_loc_name", "") + load_usr.pref_code = profile_dict.get("pref_code", 0) + load_usr.trainer_name = profile_dict.get( + "trainer_name", "Newb" + str(random.randint(1111, 999999)) + ) + load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) + load_usr.wallet = profile_dict.get("wallet", 0) + load_usr.fight_money = profile_dict.get("fight_money", 0) + load_usr.score_point = profile_dict.get("score_point", 0) + load_usr.grade_max_num = profile_dict.get("grade_max_num", 0) + load_usr.extra_counter = profile_dict.get("extra_counter", 0) + load_usr.total_play_days = profile_dict.get("total_play_days", 0) + load_usr.play_date_time = profile_dict.get("play_date_time", 0) + load_usr.lucky_box_fail_num = profile_dict.get("lucky_box_fail_num", 0) + load_usr.event_reward_get_flag = profile_dict.get("event_reward_get_flag", 0) + load_usr.rank_pvp_all = profile_dict.get("rank_pvp_all", 0) + load_usr.rank_pvp_loc = profile_dict.get("rank_pvp_loc", 0) + load_usr.rank_cpu_all = profile_dict.get("rank_cpu_all", 0) + load_usr.rank_cpu_loc = profile_dict.get("rank_cpu_loc", 0) + load_usr.rank_event = profile_dict.get("rank_event", 0) + load_usr.awake_num = profile_dict.get("awake_num", 0) + load_usr.use_support_num = profile_dict.get("use_support_num", 0) + load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) + load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0) + load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0) + load_usr.beat_num = profile_dict.get("beat_num", 0) + load_usr.title_text_id = profile_dict.get("title_text_id", 0) + load_usr.title_plate_id = profile_dict.get("title_plate_id", 0) + load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0) + load_usr.navi_trainer = profile_dict.get("navi_trainer", 0) + load_usr.navi_version_id = profile_dict.get("navi_version_id", 0) + load_usr.aid_skill = profile_dict.get("aid_skill", 0) + load_usr.comment_text_id = profile_dict.get("comment_text_id", 0) + load_usr.comment_word_id = profile_dict.get("comment_word_id", 0) + load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0) + load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0) + load_usr.wko_num = profile_dict.get("wko_num", 0) + load_usr.timeup_win_num = profile_dict.get("timeup_win_num", 0) + load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0) + load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0) + load_usr.record_flag = profile_dict.get("record_flag", 0) + load_usr.site_register_status = profile_dict.get("site_register_status", 0) + load_usr.continue_num = profile_dict.get("continue_num", 0) - load_usr.avatar_body = profile_dict.get('avatar_body', 0) - load_usr.avatar_gender = profile_dict.get('avatar_gender', 0) - load_usr.avatar_background = profile_dict.get('avatar_background', 0) - load_usr.avatar_head = profile_dict.get('avatar_head', 0) - load_usr.avatar_battleglass = profile_dict.get('avatar_battleglass', 0) - load_usr.avatar_face0 = profile_dict.get('avatar_face0', 0) - load_usr.avatar_face1 = profile_dict.get('avatar_face1', 0) - load_usr.avatar_face2 = profile_dict.get('avatar_face2', 0) - load_usr.avatar_bodyall = profile_dict.get('avatar_bodyall', 0) - load_usr.avatar_wear = profile_dict.get('avatar_wear', 0) - load_usr.avatar_accessory = profile_dict.get('avatar_accessory', 0) - load_usr.avatar_stamp = profile_dict.get('avatar_stamp', 0) + load_usr.avatar_body = profile_dict.get("avatar_body", 0) + load_usr.avatar_gender = profile_dict.get("avatar_gender", 0) + load_usr.avatar_background = profile_dict.get("avatar_background", 0) + load_usr.avatar_head = profile_dict.get("avatar_head", 0) + load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0) + load_usr.avatar_face0 = profile_dict.get("avatar_face0", 0) + load_usr.avatar_face1 = profile_dict.get("avatar_face1", 0) + load_usr.avatar_face2 = profile_dict.get("avatar_face2", 0) + load_usr.avatar_bodyall = profile_dict.get("avatar_bodyall", 0) + load_usr.avatar_wear = profile_dict.get("avatar_wear", 0) + load_usr.avatar_accessory = profile_dict.get("avatar_accessory", 0) + load_usr.avatar_stamp = profile_dict.get("avatar_stamp", 0) - load_usr.event_state = profile_dict.get('event_state', 0) - load_usr.event_id = profile_dict.get('event_id', 0) - load_usr.sp_bonus_category_id_1 = profile_dict.get('sp_bonus_category_id_1', 0) - load_usr.sp_bonus_key_value_1 = profile_dict.get('sp_bonus_key_value_1', 0) - load_usr.sp_bonus_category_id_2 = profile_dict.get('sp_bonus_category_id_2', 0) - load_usr.sp_bonus_key_value_2 = profile_dict.get('sp_bonus_key_value_2', 0) - load_usr.last_play_event_id = profile_dict.get('last_play_event_id', 0) + load_usr.event_state = profile_dict.get("event_state", 0) + load_usr.event_id = profile_dict.get("event_id", 0) + load_usr.sp_bonus_category_id_1 = profile_dict.get("sp_bonus_category_id_1", 0) + load_usr.sp_bonus_key_value_1 = profile_dict.get("sp_bonus_key_value_1", 0) + load_usr.sp_bonus_category_id_2 = profile_dict.get("sp_bonus_category_id_2", 0) + load_usr.sp_bonus_key_value_2 = profile_dict.get("sp_bonus_key_value_2", 0) + load_usr.last_play_event_id = profile_dict.get("last_play_event_id", 0) res.load_user.CopyFrom(load_usr) return res.SerializeToString() - + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -288,22 +288,30 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SAVE_CHARGE return res.SerializeToString() - def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: - return {} - - def handle_matching_start_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + def handle_matching_noop( + 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: + 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 + + def handle_matching_stop_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} diff --git a/titles/pokken/config.py b/titles/pokken/config.py index e25a8c7..84da8d2 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -59,6 +59,7 @@ class PokkenServerConfig: self.__config, "pokken", "server", "auto_register", default=True ) + class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 81b2ebc..2eb5357 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,5 +1,6 @@ from enum import Enum + class PokkenConstants: GAME_CODE = "SDAK" diff --git a/titles/pokken/database.py b/titles/pokken/database.py index c940d83..272cfd8 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -3,6 +3,7 @@ from core.config import CoreConfig from .schema import * + class PokkenData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index adfde88..bccdcaf 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -114,19 +114,19 @@ class PokkenServlet(resource.Resource): endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ pokken_request.type ].name.lower() - + self.logger.debug(pokken_request) handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: self.logger.warn(f"No handler found for message type {endpoint}") return self.base.handle_noop(pokken_request) - + self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") - + ret = handler(pokken_request) return ret - + def handle_matching(self, request: Request) -> bytes: content = request.content.getvalue() client_ip = Utils.get_ip_addr(request) @@ -135,26 +135,37 @@ class PokkenServlet(resource.Resource): 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')) + 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) + 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']}") + 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 = {} + ret = {} if "result" not in ret: ret["result"] = "true" if "data" not in ret: ret["data"] = {} if "timestamp" not in ret: ret["timestamp"] = int(datetime.now().timestamp() * 1000) - + self.logger.debug(f"Response {ret}") return json.dumps(ret).encode() diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py index 686b32f..4919ea0 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -9,19 +9,26 @@ from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata item = Table( - 'pokken_item', + "pokken_item", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), - Column('category', Integer), - Column('content', Integer), - Column('type', Integer), - UniqueConstraint('user', 'category', 'content', 'type', name='pokken_item_uk'), + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("category", Integer), + Column("content", Integer), + Column("type", Integer), + UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"), mysql_charset="utf8mb4", ) + class PokkenItemData(BaseData): """ Items obtained as rewards """ + pass diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py index aec6bd3..c84ec63 100644 --- a/titles/pokken/schema/match.py +++ b/titles/pokken/schema/match.py @@ -10,29 +10,35 @@ from core.data.schema import BaseData, metadata # Pokken sends depressingly little match data... match_data = Table( - 'pokken_match_data', + "pokken_match_data", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('num_games', Integer), - Column('play_modes', JSON), - Column('results', JSON), - Column('ex_ko_num', Integer), - Column('wko_num', Integer), - Column('timeup_win_num', Integer), - Column('cool_ko_num', Integer), - Column('perfect_ko_num', Integer), - Column('use_navi', Integer), - Column('use_navi_cloth', Integer), - Column('use_aid_skill', Integer), - Column('play_date', TIMESTAMP), + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("num_games", Integer), + Column("play_modes", JSON), + Column("results", JSON), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("use_navi", Integer), + Column("use_navi_cloth", Integer), + Column("use_aid_skill", Integer), + Column("play_date", TIMESTAMP), mysql_charset="utf8mb4", ) + class PokkenMatchData(BaseData): """ Match logs """ + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: pass @@ -43,4 +49,4 @@ class PokkenMatchData(BaseData): pass def get_matches(self, limit: int = 20) -> Optional[List[Row]]: - pass \ No newline at end of file + pass diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 871ff9d..8e536f1 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -12,150 +12,179 @@ from ..const import PokkenConstants # Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me # Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue profile = Table( - 'pokken_profile', + "pokken_profile", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), - Column('trainer_name', String(16)), # optional - Column('home_region_code', Integer), - Column('home_loc_name', String(255)), - Column('pref_code', Integer), - Column('navi_newbie_flag', Boolean), - Column('navi_enable_flag', Boolean), - Column('pad_vibrate_flag', Boolean), - Column('trainer_rank_point', Integer), - Column('wallet', Integer), - Column('fight_money', Integer), - Column('score_point', Integer), - Column('grade_max_num', Integer), - Column('extra_counter', Integer), # Optional - Column('tutorial_progress_flag', JSON), # Repeated, Integer - Column('total_play_days', Integer), - Column('play_date_time', Integer), - Column('achievement_flag', JSON), # Repeated, Integer - Column('lucky_box_fail_num', Integer), - Column('event_reward_get_flag', Integer), - Column('rank_pvp_all', Integer), - Column('rank_pvp_loc', Integer), - Column('rank_cpu_all', Integer), - Column('rank_cpu_loc', Integer), - Column('rank_event', Integer), - Column('awake_num', Integer), - Column('use_support_num', Integer), - Column('rankmatch_flag', Integer), - Column('rankmatch_max', Integer), # Optional - Column('rankmatch_progress', JSON), # Repeated, Integer - Column('rankmatch_success', Integer), # Optional - Column('beat_num', Integer), # Optional - Column('title_text_id', Integer), - Column('title_plate_id', Integer), - Column('title_decoration_id', Integer), - Column('support_pokemon_list', JSON), # Repeated, Integer - Column('support_set_1_1', Integer), # Repeated, Integer - Column('support_set_1_2', Integer), - Column('support_set_2_1', Integer), # Repeated, Integer - Column('support_set_2_2', Integer), - Column('support_set_3_1', Integer), # Repeated, Integer - Column('support_set_3_2', Integer), - Column('navi_trainer', Integer), - Column('navi_version_id', Integer), - Column('aid_skill_list', JSON), # Repeated, Integer - Column('aid_skill', Integer), - Column('comment_text_id', Integer), - Column('comment_word_id', Integer), - Column('latest_use_pokemon', Integer), - Column('ex_ko_num', Integer), - Column('wko_num', Integer), - Column('timeup_win_num', Integer), - Column('cool_ko_num', Integer), - Column('perfect_ko_num', Integer), - Column('record_flag', Integer), - Column('continue_num', Integer), - Column('avatar_body', Integer), # Optional - Column('avatar_gender', Integer), # Optional - Column('avatar_background', Integer), # Optional - Column('avatar_head', Integer), # Optional - Column('avatar_battleglass', Integer), # Optional - Column('avatar_face0', Integer), # Optional - Column('avatar_face1', Integer), # Optional - Column('avatar_face2', Integer), # Optional - Column('avatar_bodyall', Integer), # Optional - Column('avatar_wear', Integer), # Optional - Column('avatar_accessory', Integer), # Optional - Column('avatar_stamp', Integer), # Optional - Column('event_state', Integer), - Column('event_id', Integer), - Column('sp_bonus_category_id_1', Integer), - Column('sp_bonus_key_value_1', Integer), - Column('sp_bonus_category_id_2', Integer), - Column('sp_bonus_key_value_2', Integer), - Column('last_play_event_id', Integer), # Optional - Column('event_achievement_flag', JSON), # Repeated, Integer - Column('event_achievement_param', JSON), # Repeated, Integer - Column('battle_num_vs_wan', Integer), # 4? - Column('win_vs_wan', Integer), - Column('battle_num_vs_lan', Integer), # 3? - Column('win_vs_lan', Integer), - Column('battle_num_vs_cpu', Integer), # 2 - Column('win_cpu', Integer), - Column('battle_num_tutorial', Integer), # 1? - mysql_charset="utf8mb4" + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("trainer_name", String(16)), # optional + Column("home_region_code", Integer), + Column("home_loc_name", String(255)), + Column("pref_code", Integer), + Column("navi_newbie_flag", Boolean), + Column("navi_enable_flag", Boolean), + Column("pad_vibrate_flag", Boolean), + Column("trainer_rank_point", Integer), + Column("wallet", Integer), + Column("fight_money", Integer), + Column("score_point", Integer), + Column("grade_max_num", Integer), + Column("extra_counter", Integer), # Optional + Column("tutorial_progress_flag", JSON), # Repeated, Integer + Column("total_play_days", Integer), + Column("play_date_time", Integer), + Column("achievement_flag", JSON), # Repeated, Integer + Column("lucky_box_fail_num", Integer), + Column("event_reward_get_flag", Integer), + Column("rank_pvp_all", Integer), + Column("rank_pvp_loc", Integer), + Column("rank_cpu_all", Integer), + Column("rank_cpu_loc", Integer), + Column("rank_event", Integer), + Column("awake_num", Integer), + Column("use_support_num", Integer), + Column("rankmatch_flag", Integer), + Column("rankmatch_max", Integer), # Optional + Column("rankmatch_progress", JSON), # Repeated, Integer + Column("rankmatch_success", Integer), # Optional + Column("beat_num", Integer), # Optional + Column("title_text_id", Integer), + Column("title_plate_id", Integer), + Column("title_decoration_id", Integer), + Column("support_pokemon_list", JSON), # Repeated, Integer + Column("support_set_1_1", Integer), # Repeated, Integer + Column("support_set_1_2", Integer), + Column("support_set_2_1", Integer), # Repeated, Integer + Column("support_set_2_2", Integer), + Column("support_set_3_1", Integer), # Repeated, Integer + Column("support_set_3_2", Integer), + Column("navi_trainer", Integer), + Column("navi_version_id", Integer), + Column("aid_skill_list", JSON), # Repeated, Integer + Column("aid_skill", Integer), + Column("comment_text_id", Integer), + Column("comment_word_id", Integer), + Column("latest_use_pokemon", Integer), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("record_flag", Integer), + Column("continue_num", Integer), + Column("avatar_body", Integer), # Optional + Column("avatar_gender", Integer), # Optional + Column("avatar_background", Integer), # Optional + Column("avatar_head", Integer), # Optional + Column("avatar_battleglass", Integer), # Optional + Column("avatar_face0", Integer), # Optional + Column("avatar_face1", Integer), # Optional + Column("avatar_face2", Integer), # Optional + Column("avatar_bodyall", Integer), # Optional + Column("avatar_wear", Integer), # Optional + Column("avatar_accessory", Integer), # Optional + Column("avatar_stamp", Integer), # Optional + Column("event_state", Integer), + Column("event_id", Integer), + Column("sp_bonus_category_id_1", Integer), + Column("sp_bonus_key_value_1", Integer), + Column("sp_bonus_category_id_2", Integer), + Column("sp_bonus_key_value_2", Integer), + Column("last_play_event_id", Integer), # Optional + Column("event_achievement_flag", JSON), # Repeated, Integer + Column("event_achievement_param", JSON), # Repeated, Integer + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_num_tutorial", Integer), # 1? + mysql_charset="utf8mb4", ) pokemon_data = Table( - 'pokken_pokemon_data', + "pokken_pokemon_data", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('char_id', Integer, nullable=False), - Column('illustration_book_no', Integer), - Column('pokemon_exp', Integer), - Column('battle_num_vs_wan', Integer), # 4? - Column('win_vs_wan', Integer), - Column('battle_num_vs_lan', Integer), # 3? - Column('win_vs_lan', Integer), - Column('battle_num_vs_cpu', Integer), # 2 - Column('win_cpu', Integer), - Column('battle_all_num_tutorial', Integer), - Column('battle_num_tutorial', Integer), # 1? - Column('bp_point_atk', Integer), - Column('bp_point_res', Integer), - Column('bp_point_def', Integer), - Column('bp_point_sp', Integer), - UniqueConstraint('user', 'char_id', name="pokken_pokemon_data_uk"), - mysql_charset="utf8mb4" + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("char_id", Integer, nullable=False), + Column("illustration_book_no", Integer), + Column("pokemon_exp", Integer), + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_all_num_tutorial", Integer), + Column("battle_num_tutorial", Integer), # 1? + Column("bp_point_atk", Integer), + Column("bp_point_res", Integer), + Column("bp_point_def", Integer), + Column("bp_point_sp", Integer), + UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"), + mysql_charset="utf8mb4", ) + class PokkenProfileData(BaseData): def create_profile(self, user_id: int) -> Optional[int]: - sql = insert(profile).values(user = user_id) - conflict = sql.on_duplicate_key_update(user = user_id) - + sql = insert(profile).values(user=user_id) + conflict = sql.on_duplicate_key_update(user=user_id) + result = self.execute(conflict) if result is None: self.logger.error(f"Failed to create pokken profile for user {user_id}!") return None return result.lastrowid - + def set_profile_name(self, user_id: int, new_name: str) -> None: - sql = update(profile).where(profile.c.user == user_id).values(trainer_name = new_name) + sql = ( + update(profile) + .where(profile.c.user == user_id) + .values(trainer_name=new_name) + ) result = self.execute(sql) if result is None: - self.logger.error(f"Failed to update pokken profile name for user {user_id}!") - + self.logger.error( + f"Failed to update pokken profile name for user {user_id}!" + ) + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass - def add_profile_points(self, user_id: int, rank_pts: int, money: int, score_pts: int) -> None: + def add_profile_points( + self, user_id: int, rank_pts: int, money: int, score_pts: int + ) -> None: pass def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_pokemon_data(self, user_id: int, pokemon_id: int, illust_no: int, get_exp: int, atk: int, res: int, defe: int, sp: int) -> Optional[int]: + def put_pokemon_data( + self, + user_id: int, + pokemon_id: int, + illust_no: int, + get_exp: int, + atk: int, + res: int, + defe: int, + sp: int, + ) -> Optional[int]: pass def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: @@ -164,13 +193,24 @@ class PokkenProfileData(BaseData): def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: pass - def put_results(self, user_id: int, pokemon_id: int, match_type: int, match_result: int) -> None: + def put_results( + self, user_id: int, pokemon_id: int, match_type: int, match_result: int + ) -> None: """ Records the match stats (type and win/loss) for the pokemon and profile """ pass - def put_stats(self, user_id: int, exkos: int, wkos: int, timeout_wins: int, cool_kos: int, perfects: int, continues: int) -> None: + def put_stats( + self, + user_id: int, + exkos: int, + wkos: int, + timeout_wins: int, + cool_kos: int, + perfects: int, + continues: int, + ) -> None: """ Records profile stats """ diff --git a/titles/pokken/schema/static.py b/titles/pokken/schema/static.py index 63aa2dd..121ebc4 100644 --- a/titles/pokken/schema/static.py +++ b/titles/pokken/schema/static.py @@ -8,5 +8,6 @@ from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata + class PokkenStaticData(BaseData): - pass \ No newline at end of file + pass diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 35a6efc..ada40c6 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -200,18 +200,24 @@ class WaccaBase: 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 - midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + midnight_today_ts = 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 < midnight_today_ts: resp.firstLoginDaily = True - - # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak if midnight_today_ts - last_login_time > 86400: is_consec_day = False - self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) - + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) + if resp.firstLoginDaily: # TODO: Daily bonus pass @@ -624,17 +630,25 @@ class WaccaBase: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_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"] and not self.game_config.mods.infinite_tickets: + 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.logger.debug(f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}") + self.logger.debug( + f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}" + ) self.data.item.spend_ticket(new_tickets[x][0]) new_tickets.pop(x) break @@ -865,7 +879,10 @@ class WaccaBase: user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp: + 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) @@ -1055,11 +1072,18 @@ 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 + user_id, + item.itemId, + item.quantity, + 0, + [0] * 5, + [0] * 13, + 0, + 0, ) if item.quantity == 0: diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index b17602a..025c161 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -366,7 +366,7 @@ class UserEventInfo: conditions = [] for x in self.conditionInfo: conditions.append(x.make()) - + return [self.eventId, conditions] @@ -374,7 +374,7 @@ class UserEventConditionInfo: def __init__(self) -> None: self.achievementCondition = 0 self.progress = 0 - + def make(self) -> List: return [self.achievementCondition, self.progress] @@ -835,11 +835,13 @@ class LastSongDetail: class FriendScoreDetail: - def __init__(self, song_id: int = 0, difficulty: int = 1, best_score: int = 0) -> None: + def __init__( + self, song_id: int = 0, difficulty: int = 1, best_score: int = 0 + ) -> None: self.songId = song_id self.difficulty = difficulty self.bestScore = best_score - + def make(self) -> List: return [self.songId, self.difficulty, self.bestScore] diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index bf6b74b..b70ac35 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -11,7 +11,7 @@ class UserInfoUpdateRequest(BaseRequest): self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] self.unknown2: List = self.params[2] - self.datesUpdated: List[DateUpdate] = [] + self.datesUpdated: List[DateUpdate] = [] self.favoritesRemoved: List[int] = self.params[4] self.favoritesAdded: List[int] = self.params[5] diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index b7ed1fc..6eef16a 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -168,7 +168,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) - + for x in self.eventInfo: evts.append(x.make()) @@ -267,7 +267,9 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2): self, is_first_login_daily: bool = False, last_login_date: int = 0 ) -> None: super().__init__(is_first_login_daily, last_login_date) - self.unk: List = [] # Ticket info, item info, message, title, voice name (not sure how they fit...) + self.unk: List = ( + [] + ) # Ticket info, item info, message, title, voice name (not sure how they fit...) def make(self) -> Dict: super().make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 4ff942b..6ac60de 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -73,7 +73,7 @@ class WaccaLily(WaccaS): self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() - + opts = self.data.profile.get_options(req.aimeId) self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") @@ -139,7 +139,7 @@ class WaccaLily(WaccaS): if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + for opt in opts: resp.options.append(UserOption(opt["opt_id"], opt["value"])) @@ -165,17 +165,23 @@ class WaccaLily(WaccaS): 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 - midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + midnight_today_ts = 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 < midnight_today_ts: resp.firstLoginDaily = True - - # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak if midnight_today_ts - last_login_time > 86400: is_consec_day = False - self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month resp.vipInfo.pageDay = datetime.now().day diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index bb8a332..1711013 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -141,7 +141,7 @@ class WaccaReverse(WaccaLilyR): ) # For some fucking reason if this isn't here time play is disabled - resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) + resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) From fe4dfe369b62ecfd90b5aa1d4531ada16d044516 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 30 Apr 2023 01:18:00 +0000 Subject: [PATCH 195/589] Update 'readme.md' --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b1cb506..ec25191 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar. # Supported games -Games listed below have been tested and confirmed working. Only game versions older then the current one in active use in arcades (n-0) or current game versions older then a year (y-1) are supported. +Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. + Chunithm + All versions up to New!! Plus From 9d8762d3da67ecf3664018a00ede80ed5642e969 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 30 Apr 2023 01:18:00 +0000 Subject: [PATCH 196/589] Update 'readme.md' --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b1cb506..ec25191 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar. # Supported games -Games listed below have been tested and confirmed working. Only game versions older then the current one in active use in arcades (n-0) or current game versions older then a year (y-1) are supported. +Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. + Chunithm + All versions up to New!! Plus From a0793aa13a32d6ea16bfd97f130738a668619de5 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 30 Apr 2023 23:31:13 +0200 Subject: [PATCH 197/589] diva: added clear set calculation + small improvements --- core/data/schema/versions/SBZV_4_rollback.sql | 9 ++ core/data/schema/versions/SBZV_5_upgrade.sql | 9 ++ titles/diva/__init__.py | 2 +- titles/diva/base.py | 96 ++++++++++++++++--- titles/diva/schema/profile.py | 8 ++ titles/diva/schema/score.py | 9 +- 6 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 core/data/schema/versions/SBZV_4_rollback.sql create mode 100644 core/data/schema/versions/SBZV_5_upgrade.sql diff --git a/core/data/schema/versions/SBZV_4_rollback.sql b/core/data/schema/versions/SBZV_4_rollback.sql new file mode 100644 index 0000000..f56327e --- /dev/null +++ b/core/data/schema/versions/SBZV_4_rollback.sql @@ -0,0 +1,9 @@ +ALTER TABLE diva_profile + DROP cnp_cid, + DROP cnp_val, + DROP cnp_rr, + DROP cnp_sp, + DROP btn_se_eqp, + DROP sld_se_eqp, + DROP chn_sld_se_eqp, + DROP sldr_tch_se_eqp; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_5_upgrade.sql b/core/data/schema/versions/SBZV_5_upgrade.sql new file mode 100644 index 0000000..7e29f7b --- /dev/null +++ b/core/data/schema/versions/SBZV_5_upgrade.sql @@ -0,0 +1,9 @@ +ALTER TABLE diva_profile + ADD cnp_cid INT NOT NULL DEFAULT -1, + ADD cnp_val INT NOT NULL DEFAULT -1, + ADD cnp_rr INT NOT NULL DEFAULT -1, + ADD cnp_sp VARCHAR(255) NOT NULL DEFAULT "", + ADD btn_se_eqp INT NOT NULL DEFAULT -1, + ADD sld_se_eqp INT NOT NULL DEFAULT -1, + ADD chn_sld_se_eqp INT NOT NULL DEFAULT -1, + ADD sldr_tch_se_eqp INT NOT NULL DEFAULT -1; \ No newline at end of file diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index 3f193db..46ea090 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 = 4 +current_schema_version = 5 diff --git a/titles/diva/base.py b/titles/diva/base.py index 9e58269..d7303e7 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -266,16 +266,17 @@ 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_id": "1,2", + "fi_name": f"{self.core_cfg.server.name} Opening,Project DIVA Festa", + # 0=PINK, 1=GREEN + "fi_kind": "1,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_add_vp": "20,5", + "fi_mul_vp": "1,2", + "fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0", + "fi_et": "2029-01-01 00:00:00.0,2029-01-01 00:00:00.0", "fi_lut": "{self.time_lut}", } @@ -401,10 +402,10 @@ class DivaBase: response += f"&lv_pnt={profile['lv_pnt']}" response += f"&vcld_pts={profile['vcld_pts']}" response += f"&skn_eqp={profile['use_pv_skn_eqp']}" - response += f"&btn_se_eqp={profile['use_pv_btn_se_eqp']}" - response += f"&sld_se_eqp={profile['use_pv_sld_se_eqp']}" - response += f"&chn_sld_se_eqp={profile['use_pv_chn_sld_se_eqp']}" - response += f"&sldr_tch_se_eqp={profile['use_pv_sldr_tch_se_eqp']}" + response += f"&btn_se_eqp={profile['btn_se_eqp']}" + response += f"&sld_se_eqp={profile['sld_se_eqp']}" + response += f"&chn_sld_se_eqp={profile['chn_sld_se_eqp']}" + response += f"&sldr_tch_se_eqp={profile['sldr_tch_se_eqp']}" response += f"&passwd_stat={profile['passwd_stat']}" # Store stuff to add to rework @@ -478,6 +479,21 @@ class DivaBase: response += f"&dsp_clr_sts={profile['dsp_clr_sts']}" response += f"&rgo_sts={profile['rgo_sts']}" + # Contest progress + response += f"&cv_cid=-1,-1,-1,-1" + response += f"&cv_sc=-1,-1,-1,-1" + response += f"&cv_bv=-1,-1,-1,-1" + response += f"&cv_bv=-1,-1,-1,-1" + response += f"&cv_bf=-1,-1,-1,-1" + + # Contest now playing id, return -1 if no current playing contest + response += f"&cnp_cid={profile['cnp_cid']}" + response += f"&cnp_val={profile['cnp_val']}" + # border can be 0=bronzem 1=silver, 2=gold + response += f"&cnp_rr={profile['cnp_rr']}" + # only show contest specifier if it is not empty + response += f"&cnp_sp={profile['cnp_sp']}" if profile["cnp_sp"] != "" else "" + # To be fully fixed if "my_qst_id" not in profile: response += f"&my_qst_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" @@ -488,7 +504,63 @@ class DivaBase: response += f"&my_qst_prgrs=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" response += f"&my_qst_et=2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2100-01-01%2008%3A59%3A59.0,2100-01-01%2008%3A59%3A59.0,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx" - response += f"&clr_sts=0,0,0,0,0,0,0,0,56,52,35,6,6,3,1,0,0,0,0,0" + + # define a helper class to store all counts for clear, great, + # excellent and perfect + class ClearSet: + def __init__(self): + self.clear = 0 + self.great = 0 + self.excellent = 0 + self.perfect = 0 + + # create a dict to store the ClearSets per difficulty + clear_set_dict = { + 0: ClearSet(), # easy + 1: ClearSet(), # normal + 2: ClearSet(), # hard + 3: ClearSet(), # extreme + 4: ClearSet(), # exExtreme + } + + # get clear status from user scores + pv_records = self.data.score.get_best_scores(data["pd_id"]) + clear_status = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + + if pv_records is not None: + for score in pv_records: + if score["edition"] == 0: + # cheap and standard both count to "clear" + if score["clr_kind"] in {1, 2}: + clear_set_dict[score["difficulty"]].clear += 1 + elif score["clr_kind"] == 3: + clear_set_dict[score["difficulty"]].great += 1 + elif score["clr_kind"] == 4: + clear_set_dict[score["difficulty"]].excellent += 1 + elif score["clr_kind"] == 5: + clear_set_dict[score["difficulty"]].perfect += 1 + else: + # 4=ExExtreme + if score["clr_kind"] in {1, 2}: + clear_set_dict[4].clear += 1 + elif score["clr_kind"] == 3: + clear_set_dict[4].great += 1 + elif score["clr_kind"] == 4: + clear_set_dict[4].excellent += 1 + elif score["clr_kind"] == 5: + clear_set_dict[4].perfect += 1 + + # now add all values to a list + clear_list = [] + for clear_set in clear_set_dict.values(): + clear_list.append(clear_set.clear) + clear_list.append(clear_set.great) + clear_list.append(clear_set.excellent) + clear_list.append(clear_set.perfect) + + clear_status = ",".join(map(str, clear_list)) + + response += f"&clr_sts={clear_status}" # Store stuff to add to rework response += f"&mdl_eqp_tm={self.time_lut}" diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 1a498e2..7107068 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -34,9 +34,17 @@ profile = Table( 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("btn_se_eqp", Integer, nullable=False, server_default="-1"), + Column("sld_se_eqp", Integer, nullable=False, server_default="-1"), + Column("chn_sld_se_eqp", Integer, nullable=False, server_default="-1"), + Column("sldr_tch_se_eqp", Integer, nullable=False, server_default="-1"), 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"), + Column("cnp_cid", Integer, nullable=False, server_default="-1"), + Column("cnp_val", Integer, nullable=False, server_default="-1"), + Column("cnp_rr", Integer, nullable=False, server_default="-1"), + Column("cnp_sp", String(255), nullable=False, server_default=""), Column("dsp_clr_brdr", Integer, nullable=False, server_default="7"), Column("dsp_intrm_rnk", Integer, nullable=False, server_default="1"), Column("dsp_clr_sts", Integer, nullable=False, server_default="1"), diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index 2d86925..2171659 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -3,6 +3,7 @@ from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row from typing import Optional, List, Dict, Any from core.data.schema import BaseData, metadata @@ -167,7 +168,7 @@ class DivaScoreData(BaseData): def get_best_user_score( self, user_id: int, pv_id: int, difficulty: int, edition: int - ) -> Optional[Dict]: + ) -> Optional[Row]: sql = score.select( and_( score.c.user == user_id, @@ -184,7 +185,7 @@ class DivaScoreData(BaseData): def get_top3_scores( self, pv_id: int, difficulty: int, edition: int - ) -> Optional[List[Dict]]: + ) -> Optional[List[Row]]: sql = ( score.select( and_( @@ -204,7 +205,7 @@ class DivaScoreData(BaseData): def get_global_ranking( self, user_id: int, pv_id: int, difficulty: int, edition: int - ) -> Optional[List]: + ) -> Optional[List[Row]]: # get the subquery max score of a user with pv_id, difficulty and # edition sql_sub = ( @@ -231,7 +232,7 @@ class DivaScoreData(BaseData): return None return result.fetchone() - def get_best_scores(self, user_id: int) -> Optional[List]: + def get_best_scores(self, user_id: int) -> Optional[List[Row]]: sql = score.select(score.c.user == user_id) result = self.execute(sql) From deeac1d8dbe163e3abe090826d16b50af0809cb2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 30 Apr 2023 22:19:31 -0400 Subject: [PATCH 198/589] add finale handler, pre-dx game codes --- titles/mai2/__init__.py | 11 +++++++- titles/mai2/base.py | 3 +-- titles/mai2/const.py | 41 +++++++++++++++++++++++++++++- titles/mai2/dx.py | 15 +++++++++++ titles/mai2/{plus.py => dxplus.py} | 2 +- titles/mai2/finale.py | 15 +++++++++++ titles/mai2/index.py | 24 ++++++++++++++--- 7 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 titles/mai2/dx.py rename titles/mai2/{plus.py => dxplus.py} (93%) create mode 100644 titles/mai2/finale.py diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 810eac9..0a76de8 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -6,5 +6,14 @@ from titles.mai2.read import Mai2Reader index = Mai2Servlet database = Mai2Data reader = Mai2Reader -game_codes = [Mai2Constants.GAME_CODE] +game_codes = [ + Mai2Constants.GAME_CODE_DX, + Mai2Constants.GAME_CODE_FINALE, + Mai2Constants.GAME_CODE_MILK, + Mai2Constants.GAME_CODE_MURASAKI, + Mai2Constants.GAME_CODE_PINK, + Mai2Constants.GAME_CODE_ORANGE, + Mai2Constants.GAME_CODE_GREEN, + Mai2Constants.GAME_CODE, +] current_schema_version = 4 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 171378c..efa30a0 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -12,8 +12,7 @@ class Mai2Base: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: self.core_config = cfg self.game_config = game_cfg - self.game = Mai2Constants.GAME_CODE - self.version = Mai2Constants.VER_MAIMAI_DX + self.version = Mai2Constants.VER_MAIMAI self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") diff --git a/titles/mai2/const.py b/titles/mai2/const.py index dcc7e29..19cb867 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -20,10 +20,31 @@ class Mai2Constants: DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" - GAME_CODE = "SDEZ" + GAME_CODE = "SBXL" + GAME_CODE_GREEN = "SBZF" + GAME_CODE_ORANGE = "SDBM" + GAME_CODE_PINK = "SDCQ" + GAME_CODE_MURASAKI = "SDDK" + GAME_CODE_MILK = "SDDZ" + GAME_CODE_FINALE = "SDEY" + GAME_CODE_DX = "SDEZ" CONFIG_NAME = "mai2.yaml" + VER_MAIMAI = 1000 + VER_MAIMAI_PLUS = 1001 + VER_MAIMAI_GREEN = 1002 + VER_MAIMAI_GREEN_PLUS = 1003 + VER_MAIMAI_ORANGE = 1004 + VER_MAIMAI_ORANGE_PLUS = 1005 + VER_MAIMAI_PINK = 1006 + VER_MAIMAI_PINK_PLUS = 1007 + VER_MAIMAI_MURASAKI = 1008 + VER_MAIMAI_MURASAKI_PLUS = 1009 + VER_MAIMAI_MILK = 1010 + VER_MAIMAI_MILK_PLUS = 1011 + VER_MAIMAI_FINALE = 1012 + VER_MAIMAI_DX = 0 VER_MAIMAI_DX_PLUS = 1 VER_MAIMAI_DX_SPLASH = 2 @@ -42,6 +63,24 @@ class Mai2Constants: "maimai DX Festival", ) + VERSION_STRING_OLD = ( + "maimai", + "maimai PLUS", + "maimai GreeN", + "maimai GreeN PLUS", + "maimai ORANGE", + "maimai ORANGE PLUS", + "maimai PiNK", + "maimai PiNK PLUS", + "maimai MURASAKi", + "maimai MURASAKi PLUS", + "maimai MiLK", + "maimai MiLK PLUS", + "maimai FiNALE", + ) + @classmethod def game_ver_to_string(cls, ver: int): + if ver >= 1000: + return cls.VERSION_STRING_OLD[ver / 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py new file mode 100644 index 0000000..9a9cae7 --- /dev/null +++ b/titles/mai2/dx.py @@ -0,0 +1,15 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2DX(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX diff --git a/titles/mai2/plus.py b/titles/mai2/dxplus.py similarity index 93% rename from titles/mai2/plus.py rename to titles/mai2/dxplus.py index a3c9288..64c9297 100644 --- a/titles/mai2/plus.py +++ b/titles/mai2/dxplus.py @@ -9,7 +9,7 @@ from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Plus(Mai2Base): +class Mai2DXPlus(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/finale.py b/titles/mai2/finale.py new file mode 100644 index 0000000..bb4b67e --- /dev/null +++ b/titles/mai2/finale.py @@ -0,0 +1,15 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2Finale(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_FINALE diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 1b92842..9eb3b52 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -14,7 +14,9 @@ from core.utils import Utils from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants from titles.mai2.base import Mai2Base -from titles.mai2.plus import Mai2Plus +from titles.mai2.finale import Mai2Finale +from titles.mai2.dx import Mai2DX +from titles.mai2.dxplus import Mai2DXPlus from titles.mai2.splash import Mai2Splash from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.universe import Mai2Universe @@ -32,8 +34,8 @@ class Mai2Servlet: ) self.versions = [ - Mai2Base, - Mai2Plus, + Mai2DX, + Mai2DXPlus, Mai2Splash, Mai2SplashPlus, Mai2Universe, @@ -41,6 +43,22 @@ class Mai2Servlet: Mai2Festival, ] + self.versions_old = [ + Mai2Base, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Mai2Finale, + ] + self.logger = logging.getLogger("mai2") log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) From 3b6fc6618c8e657edf912200c4df5360df4898fd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 2 May 2023 23:55:57 -0400 Subject: [PATCH 199/589] fix naomitest --- index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.py b/index.py index 11fad94..7199dbe 100644 --- a/index.py +++ b/index.py @@ -42,7 +42,7 @@ class HttpDispatcher(resource.Resource): conditions=dict(method=["POST"]), ) - self.map_post.connect( + self.map_get.connect( "allnet_ping", "/naomitest.html", controller="allnet", From 8d94d25893b41db9c5f1f4293eba86225f651169 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 03:25:29 -0400 Subject: [PATCH 200/589] mai2: add version seperators --- titles/mai2/index.py | 104 +++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 9eb3b52..60a618f 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -60,27 +60,29 @@ 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, - ) + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), + encoding="utf8", + when="d", + backupCount=10, + ) - fileHandler.setFormatter(log_fmt) + fileHandler.setFormatter(log_fmt) - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) - self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install( - level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str - ) + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True @classmethod def get_allnet_info( @@ -100,13 +102,13 @@ class Mai2Servlet: return ( True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - f"{core_cfg.title.hostname}:{core_cfg.title.port}/", + f"{core_cfg.title.hostname}", ) return ( True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", - f"{core_cfg.title.hostname}/", + f"{core_cfg.title.hostname}", ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: @@ -120,21 +122,50 @@ class Mai2Servlet: endpoint = url_split[len(url_split) - 1] client_ip = Utils.get_ip_addr(request) - if version < 105: # 1.0 - internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # Universe - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125 and version < 130: # Universe Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - elif version >= 130: # Festival - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + if request.uri.startswith(b"/SDEY"): + if version < 105: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 105 and version < 110: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 110 and version < 115: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 115 and version < 120: # Splash Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # Universe + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # Universe Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130: # Festival + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + else: + if version < 110: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI + elif version >= 110 and version < 120: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_PLUS + elif version >= 120 and version < 130: # Green + internal_ver = Mai2Constants.VER_MAIMAI_GREEN + elif version >= 130 and version < 140: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS + elif version >= 140 and version < 150: # Orange + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE + elif version >= 150 and version < 160: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS + elif version >= 160 and version < 170: # Pink + internal_ver = Mai2Constants.VER_MAIMAI_PINK + elif version >= 170 and version < 180: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS + elif version >= 180 and version < 185: # Murasaki + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI + elif version >= 185 and version < 190: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS + elif version >= 190 and version < 195: # Milk + internal_ver = Mai2Constants.VER_MAIMAI_MILK + elif version >= 195 and version < 197: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS + elif version >= 197: # Finale + internal_ver = Mai2Constants.VER_MAIMAI_FINALE + 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 @@ -156,7 +187,12 @@ class Mai2Servlet: 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 internal_ver >= Mai2Constants.VER_MAIMAI: + handler_cls = self.versions_old[internal_ver](self.core_cfg, self.game_cfg) + + else: + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") From 6d1855a6bc910d9a042b8bb4c1c220d958d4be05 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 03:25:55 -0400 Subject: [PATCH 201/589] billing: handle malformed requests --- core/allnet.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 119f0ae..ab435e7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -249,14 +249,18 @@ class AllnetServlet: signer = PKCS1_v1_5.new(rsa) digest = SHA.new() - kc_playlimit = int(req_dict[0]["playlimit"]) - kc_nearfull = int(req_dict[0]["nearfull"]) - kc_billigtype = int(req_dict[0]["billingtype"]) - kc_playcount = int(req_dict[0]["playcnt"]) - kc_serial: str = req_dict[0]["keychipid"] - kc_game: str = req_dict[0]["gameid"] - kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") - kc_serial_bytes = kc_serial.encode() + try: + kc_playlimit = int(req_dict[0]["playlimit"]) + kc_nearfull = int(req_dict[0]["nearfull"]) + kc_billigtype = int(req_dict[0]["billingtype"]) + kc_playcount = int(req_dict[0]["playcnt"]) + kc_serial: str = req_dict[0]["keychipid"] + kc_game: str = req_dict[0]["gameid"] + kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") + kc_serial_bytes = kc_serial.encode() + + except KeyError as e: + return f"result=5&linelimit=&message={e} field is missing".encode() machine = self.data.arcade.get_machine(kc_serial) if machine is None and not self.config.server.allow_unregistered_serials: From 7bb8c2c80c4ade483efc7cd02ce21d98d76dfc11 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 03:25:55 -0400 Subject: [PATCH 202/589] billing: handle malformed requests --- core/allnet.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 119f0ae..ab435e7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -249,14 +249,18 @@ class AllnetServlet: signer = PKCS1_v1_5.new(rsa) digest = SHA.new() - kc_playlimit = int(req_dict[0]["playlimit"]) - kc_nearfull = int(req_dict[0]["nearfull"]) - kc_billigtype = int(req_dict[0]["billingtype"]) - kc_playcount = int(req_dict[0]["playcnt"]) - kc_serial: str = req_dict[0]["keychipid"] - kc_game: str = req_dict[0]["gameid"] - kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") - kc_serial_bytes = kc_serial.encode() + try: + kc_playlimit = int(req_dict[0]["playlimit"]) + kc_nearfull = int(req_dict[0]["nearfull"]) + kc_billigtype = int(req_dict[0]["billingtype"]) + kc_playcount = int(req_dict[0]["playcnt"]) + kc_serial: str = req_dict[0]["keychipid"] + kc_game: str = req_dict[0]["gameid"] + kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") + kc_serial_bytes = kc_serial.encode() + + except KeyError as e: + return f"result=5&linelimit=&message={e} field is missing".encode() machine = self.data.arcade.get_machine(kc_serial) if machine is None and not self.config.server.allow_unregistered_serials: From d0242b456db2cce1d3c98097ecd68158d6e854e4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 22:29:08 -0400 Subject: [PATCH 203/589] mai2: fix for dx 1.00 --- titles/mai2/base.py | 9 ++++++++- titles/mai2/index.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 171378c..44ec60d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -16,8 +16,15 @@ class Mai2Base: self.version = Mai2Constants.VER_MAIMAI_DX self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + + if self.core_config.server.is_develop and self.core_config.title.port > 0: + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/100/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/SDEY/100/" def handle_get_game_setting_api_request(self, data: Dict): + # TODO: See if making this epoch 0 breaks things reboot_start = date.strftime( datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT ) @@ -34,7 +41,7 @@ class Mai2Base: "movieStatus": 0, "movieServerUri": "", "deliverServerUri": "", - "oldServerUri": "", + "oldServerUri": self.old_server, "usbDlServerUri": "", "rebootInterval": 0, }, diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 1b92842..3326843 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -82,13 +82,13 @@ class Mai2Servlet: return ( True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - f"{core_cfg.title.hostname}:{core_cfg.title.port}/", + 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}/", + f"{core_cfg.title.hostname}", ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: From efd8f86e48e533d7bfd96c6be6b69bda78783af7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 09:46:16 -0400 Subject: [PATCH 204/589] re-add docker files for #19 --- Dockerfile | 21 +++++++++++++++++ docker-compose.yml | 57 ++++++++++++++++++++++++++++++++++++++++++++++ entrypoint.sh | 11 +++++++++ 3 files changed, 89 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e34dfec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.9.15-slim-bullseye + +RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm -y + +WORKDIR /app +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt +RUN npm i -g nodemon + +COPY entrypoint.sh entrypoint.sh +RUN chmod +x entrypoint.sh + +COPY index.py index.py +COPY dbutils.py dbutils.py +ADD core core +ADD titles titles +ADD config config +ADD log log +ADD cert cert + +ENTRYPOINT [ "/app/entrypoint.sh" ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c43b6e5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3.9" +services: + app: + hostname: ma.app + build: . + volumes: + - ./aime:/app/aime + + environment: + CFG_DEV: 1 + CFG_CORE_SERVER_HOSTNAME: 0.0.0.0 + CFG_CORE_DATABASE_HOST: ma.db + CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached + CFG_CORE_AIMEDB_KEY: keyhere + CFG_CHUNI_SERVER_LOGLEVEL: debug + + ports: + - "80:80" + - "8443:8443" + - "22345:22345" + + - "8080:8080" + - "8090:8090" + + depends_on: + db: + condition: service_healthy + + db: + hostname: ma.db + image: mysql:8.0.31-debian + environment: + MYSQL_DATABASE: aime + MYSQL_USER: aime + MYSQL_PASSWORD: aime + MYSQL_ROOT_PASSWORD: AimeRootPassword + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 5s + retries: 5 + + + memcached: + hostname: ma.memcached + image: memcached:1.6.17-bullseye + + phpmyadmin: + hostname: ma.phpmyadmin + image: phpmyadmin:latest + environment: + PMA_HOSTS: ma.db + PMA_USER: root + PMA_PASSWORD: AimeRootPassword + APACHE_PORT: 8080 + ports: + - "8080:8080" + diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..aa95ca8 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [[ -z "${CFG_DEV}" ]]; then + echo Production mode + python3 index.py +else + echo Development mode + python3 dbutils.py create + nodemon -w aime --legacy-watch index.py +fi + From e3b1addce62a1b6763547f379be500932b4ae83f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:12:31 -0400 Subject: [PATCH 205/589] mai2: fix up version comments --- titles/mai2/index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 249b3af..4d6a177 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -145,23 +145,23 @@ class Mai2Servlet: internal_ver = Mai2Constants.VER_MAIMAI_PLUS elif version >= 120 and version < 130: # Green internal_ver = Mai2Constants.VER_MAIMAI_GREEN - elif version >= 130 and version < 140: # Plus + elif version >= 130 and version < 140: # Green Plus internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS elif version >= 140 and version < 150: # Orange internal_ver = Mai2Constants.VER_MAIMAI_ORANGE - elif version >= 150 and version < 160: # Plus + elif version >= 150 and version < 160: # Orange Plus internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS elif version >= 160 and version < 170: # Pink internal_ver = Mai2Constants.VER_MAIMAI_PINK - elif version >= 170 and version < 180: # Plus + elif version >= 170 and version < 180: # Pink Plus internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS elif version >= 180 and version < 185: # Murasaki internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI - elif version >= 185 and version < 190: # Plus + elif version >= 185 and version < 190: # Murasaki Plus internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS elif version >= 190 and version < 195: # Milk internal_ver = Mai2Constants.VER_MAIMAI_MILK - elif version >= 195 and version < 197: # Plus + elif version >= 195 and version < 197: # Milk Plus internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS elif version >= 197: # Finale internal_ver = Mai2Constants.VER_MAIMAI_FINALE From dcff8adbab364d325c18ed0afd9db6d3ac3c69ac Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:22:41 -0400 Subject: [PATCH 206/589] mai2: update documentation --- docs/game_specific_info.md | 49 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f1b334e..3260118 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -105,28 +105,48 @@ Config file is located in `config/cxb.yaml`. ### 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 | -| 6 | maimai DX Festival | +| Game Code | Version ID | Version Name | +|-----------|------------|-------------------------| +| SDEZ | 0 | maimai DX | +| SDEZ | 1 | maimai DX PLUS | +| SDEZ | 2 | maimai DX Splash | +| SDEZ | 3 | maimai DX Splash PLUS | +| SDEZ | 4 | maimai DX Universe | +| SDEZ | 5 | maimai DX Universe PLUS | +| SDEZ | 6 | maimai DX Festival | + +For versions pre-dx +| Game Code | Version ID | Version Name | +|-----------|------------|----------------------| +| SBXL | 1000 | maimai | +| SBXL | 1001 | maimai PLUS | +| SBZF | 1002 | maimai GreeN | +| SBZF | 1003 | maimai GreeN PLUS | +| SDBM | 1004 | maimai ORANGE | +| SDBM | 1005 | maimai ORANGE PLUS | +| SDCQ | 1006 | maimai PiNK | +| SDCQ | 1007 | maimai PiNK PLUS | +| SDDK | 1008 | maimai MURASAKI | +| SDDK | 1009 | maimai MURASAKI PLUS | +| SDDZ | 1010 | maimai MILK | +| SDDZ | 1011 | maimai MILK PLUS | +| SDEY | 1012 | maimai FiNALE | ### Importer In order to use the importer locate your game installation folder and execute: - +DX: ```shell -python read.py --series SDEZ --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +python read.py --series --version --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder +``` +Pre-DX: +```shell +python read.py --series --version --binfolder /path/to/data --optfolder /path/to/patch/data ``` - The importer for maimai DX will import Events, Music and Tickets. +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. -**NOTE: It is required to use the importer because the game will -crash without Events!** +**NOTE: It is required to use the importer because some games will not function properly or even crash without Events!** ### Database upgrade @@ -135,6 +155,7 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core ```shell python dbutils.py --game SDEZ upgrade ``` +Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ## Hatsune Miku Project Diva From 989c08065749aa6cb01fc819f784ebd219164997 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:25:14 -0400 Subject: [PATCH 207/589] mai2: further documentation clarification --- docs/game_specific_info.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 3260118..167dc8b 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -144,9 +144,10 @@ Pre-DX: python read.py --series --version --binfolder /path/to/data --optfolder /path/to/patch/data ``` The importer for maimai DX will import Events, Music and Tickets. -The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. -**NOTE: It is required to use the importer because some games will not function properly or even crash without Events!** +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. + +**Important: It is required to use the importer because some games may not function properly or even crash without Events!** ### Database upgrade From 8b9771b5af3cc2a80643be53f2e69ae201b2df80 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:24:47 -0400 Subject: [PATCH 208/589] mai2: implement event reader for pre-dx games --- docs/game_specific_info.md | 2 +- titles/mai2/const.py | 2 +- titles/mai2/read.py | 157 ++++++++++++++++++++++++++++++++++--- 3 files changed, 149 insertions(+), 12 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 167dc8b..a2a916a 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -145,7 +145,7 @@ python read.py --series --version --binfolder /path/to/ ``` The importer for maimai DX will import Events, Music and Tickets. -The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. For games that do use encryption, provide the key, as a hex string, with the `--extra` flag. Ex `--extra 00112233445566778899AABBCCDDEEFF` **Important: It is required to use the importer because some games may not function properly or even crash without Events!** diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 19cb867..6d638b9 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -82,5 +82,5 @@ class Mai2Constants: @classmethod def game_ver_to_string(cls, ver: int): if ver >= 1000: - return cls.VERSION_STRING_OLD[ver / 1000] + return cls.VERSION_STRING_OLD[ver - 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 5809464..daa908f 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -4,6 +4,9 @@ import os import re import xml.etree.ElementTree as ET from typing import Any, Dict, List, Optional +from Crypto.Cipher import AES +import zlib +import codecs from core.config import CoreConfig from core.data import Data @@ -34,18 +37,139 @@ class Mai2Reader(BaseReader): def read(self) -> None: data_dirs = [] - if self.bin_dir is not None: - data_dirs += self.get_data_directories(self.bin_dir) + if self.version < Mai2Constants.VER_MAIMAI: + 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) + if self.opt_dir is not None: + data_dirs += self.get_data_directories(self.opt_dir) - for dir in data_dirs: - self.logger.info(f"Read from {dir}") - self.get_events(f"{dir}/event") - self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") - self.read_music(f"{dir}/music") - self.read_tickets(f"{dir}/ticket") + for dir in data_dirs: + self.logger.info(f"Read from {dir}") + self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") + self.read_music(f"{dir}/music") + self.read_tickets(f"{dir}/ticket") + + else: + self.logger.warn("Pre-DX Readers are not yet implemented!") + if not os.path.exists(f"{self.bin_dir}/tables"): + self.logger.error(f"tables directory not found in {self.bin_dir}") + return + + if self.version >= Mai2Constants.VER_MAIMAI_MILK: + if self.extra is None: + self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag") + return + + key = bytes.fromhex(self.extra) + + else: + key = None + + evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) + + return + + def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]: + if not os.path.exists(f"{dir}/{file}"): + self.logger.warn(f"file {file} does not exist in directory {dir}, skipping") + return + + self.logger.info(f"Load table {file} from {dir}") + if key is not None: + cipher = AES.new(key, AES.MODE_CBC) + with open(f"{dir}/{file}", "rb") as f: + f_encrypted = f.read() + f_data = cipher.decrypt(f_encrypted)[0x10:] + + else: + with open(f"{dir}/{file}", "rb") as f: + f_data = f.read()[0x10:] + + if f_data is None or not f_data: + self.logger.warn(f"file {dir} could not be read, skipping") + return + + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16) + f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] + f_split = f_decoded.splitlines() + + has_struct_def = "struct " in f_decoded + is_struct = False + struct_def = [] + tbl_content = [] + + if has_struct_def: + for x in f_split: + if x.startswith("struct "): + is_struct = True + struct_name = x[7:-1] + continue + + if x.startswith("};"): + is_struct = False + break + + if is_struct: + try: + struct_def.append(x[x.rindex(" ") + 2: -1]) + except ValueError: + self.logger.warn(f"rindex failed on line {x}") + + if is_struct: + self.logger.warn("Struct not formatted properly") + + if not struct_def: + self.logger.warn("Struct def not found") + + name = file[:file.index(".")] + if "_" in name: + name = name[:file.index("_")] + + for x in f_split: + if not x.startswith(name.upper()): + continue + + line_match = re.match(r"(\w+)\((.*?)\)([ ]+\/{3}<[ ]+(.*))?", x) + if line_match is None: + continue + + if not line_match.group(1) == name.upper(): + self.logger.warn(f"Strange regex match for line {x} -> {line_match}") + continue + + vals = line_match.group(2) + comment = line_match.group(4) + line_dict = {} + + vals_split = vals.split(",") + for y in range(len(vals_split)): + stripped = vals_split[y].strip().lstrip("L\"").lstrip("\"").rstrip("\"") + if not stripped or stripped is None: + continue + + if has_struct_def and len(struct_def) > y: + line_dict[struct_def[y]] = stripped + + else: + line_dict[f'item_{y}'] = stripped + + if comment: + line_dict['comment'] = comment + + tbl_content.append(line_dict) + + if tbl_content: + return tbl_content + + else: + self.logger.warning("Failed load table content, skipping") + return def get_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -188,3 +312,16 @@ class Mai2Reader(BaseReader): self.version, id, ticket_type, price, name ) self.logger.info(f"Added ticket {id}...") + + def read_old_events(self, events: List[Dict[str, str]]) -> None: + for event in events: + evt_id = int(event.get('イベントID', '0')) + evt_expire_time = float(event.get('オフ時強制時期', '0.0')) + is_exp = bool(int(event.get('海外許可', '0'))) + is_aou = bool(int(event.get('AOU許可', '0'))) + name = event.get('comment', f'evt_{evt_id}') + + self.data.static.put_game_event(self.version, 0, evt_id, name) + + if not (is_exp or is_aou): + self.data.static.toggle_game_event(self.version, evt_id, False) \ No newline at end of file From cad523dfceb4c2833d7a79361da5aa41b9f1f3f9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:36:07 -0400 Subject: [PATCH 209/589] mai2: add patch reader --- titles/mai2/read.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index daa908f..f4b17cd 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -72,6 +72,13 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + + if self.opt_dir is not None: + evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) return @@ -95,7 +102,7 @@ class Mai2Reader(BaseReader): self.logger.warn(f"file {dir} could not be read, skipping") return - f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16) + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] f_split = f_decoded.splitlines() @@ -313,7 +320,10 @@ class Mai2Reader(BaseReader): ) self.logger.info(f"Added ticket {id}...") - def read_old_events(self, events: List[Dict[str, str]]) -> None: + def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: + if events is None: + return + for event in events: evt_id = int(event.get('イベントID', '0')) evt_expire_time = float(event.get('オフ時強制時期', '0.0')) @@ -324,4 +334,9 @@ class Mai2Reader(BaseReader): self.data.static.put_game_event(self.version, 0, evt_id, name) if not (is_exp or is_aou): - self.data.static.toggle_game_event(self.version, evt_id, False) \ No newline at end of file + self.data.static.toggle_game_event(self.version, evt_id, False) + + def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: + if scores is None or text is None: + return + # TODO From 8149f09a408b79aa575ebaece6578b363831c95a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:37:05 -0400 Subject: [PATCH 210/589] mai2: stub music reader --- titles/mai2/read.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index f4b17cd..a7e10de 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -72,6 +72,7 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) if self.opt_dir is not None: evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) @@ -79,6 +80,7 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) return From b34b441ba8b97cd9c3e49e0f404c846c3807ee78 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 6 May 2023 19:04:10 -0400 Subject: [PATCH 211/589] mai2: reimplement pre-dx versions --- core/data/schema/versions/SDEZ_4_rollback.sql | 26 + core/data/schema/versions/SDEZ_5_upgrade.sql | 17 + titles/mai2/base.py | 40 +- titles/mai2/const.py | 61 +- titles/mai2/dx.py | 725 ++++++++++++++++++ titles/mai2/dxplus.py | 4 +- titles/mai2/festival.py | 4 +- titles/mai2/index.py | 34 +- titles/mai2/splash.py | 4 +- titles/mai2/splashplus.py | 4 +- titles/mai2/universe.py | 177 +---- titles/mai2/universeplus.py | 4 +- 12 files changed, 840 insertions(+), 260 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_4_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_5_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_4_rollback.sql b/core/data/schema/versions/SDEZ_4_rollback.sql new file mode 100644 index 0000000..290fa85 --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_rollback.sql @@ -0,0 +1,26 @@ +DELETE FROM mai2_static_event WHERE version < 13; +UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_music WHERE version < 13; +UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_ticket WHERE version < 13; +UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_cards WHERE version < 13; +UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_detail WHERE version < 13; +UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_extend WHERE version < 13; +UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_option WHERE version < 13; +UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_ghost WHERE version < 13; +UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_rating WHERE version < 13; +UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13; diff --git a/core/data/schema/versions/SDEZ_5_upgrade.sql b/core/data/schema/versions/SDEZ_5_upgrade.sql new file mode 100644 index 0000000..18ba4ac --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_upgrade.sql @@ -0,0 +1,17 @@ +UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000; \ No newline at end of file diff --git a/titles/mai2/base.py b/titles/mai2/base.py index dcb3bcc..7cfdb93 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -17,18 +17,18 @@ class Mai2Base: self.logger = logging.getLogger("mai2") if self.core_config.server.is_develop and self.core_config.title.port > 0: - self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" else: - self.old_server = f"http://{self.core_config.title.hostname}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): # TODO: See if making this epoch 0 breaks things reboot_start = date.strftime( - datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT + datetime.fromtimestamp(0.0), Mai2Constants.DATE_TIME_FORMAT ) reboot_end = date.strftime( - datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT + datetime.fromtimestamp(0.0) + timedelta(hours=1), Mai2Constants.DATE_TIME_FORMAT ) return { "gameSetting": { @@ -39,9 +39,9 @@ class Mai2Base: "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", - "deliverServerUri": "", - "oldServerUri": self.old_server, - "usbDlServerUri": "", + "deliverServerUri": self.old_server + "deliver", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl", "rebootInterval": 0, }, "isAouAccession": "true", @@ -56,8 +56,9 @@ class Mai2Base: def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) + print(self.version) events_lst = [] - if events is None: + if events is None or not events: self.logger.warn("No enabled events, did you run the reader?") return {"type": data["type"], "length": 0, "gameEventList": []} @@ -127,28 +128,20 @@ class Mai2Base: "userId": data["userId"], "userName": profile["userName"], "isLogin": False, - "lastGameId": profile["lastGameId"], "lastDataVersion": profile["lastDataVersion"], - "lastRomVersion": profile["lastRomVersion"], "lastLoginDate": profile["lastLoginDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused + "nameplateId": 0, # Unused + "frameId": profile["frameId"], "iconId": profile["iconId"], "trophyId": 0, # Unused "partnerId": profile["partnerId"], - "frameId": profile["frameId"], - "dispRate": option[ - "dispRate" - ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end - "totalAwake": profile["totalAwake"], - "isNetMember": profile["isNetMember"], - "dailyBonusDate": profile["dailyBonusDate"], - "headPhoneVolume": option["headPhoneVolume"], - "isInherit": False, # Not sure what this is or does?? - "banState": profile["banState"] - if profile["banState"] is not None - else 0, # New with uni+ + "dispRate": option["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide + "dispRank": 0, # TODO + "dispHomeRanker": 0, # TODO + "dispTotalLv": 0, # TODO + "totalLv": 0, # TODO } def handle_user_login_api_request(self, data: Dict) -> Dict: @@ -169,7 +162,6 @@ class Mai2Base: "lastLoginDate": lastLoginDate, "loginCount": loginCt, "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: diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 6d638b9..d8f5941 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -31,39 +31,29 @@ class Mai2Constants: CONFIG_NAME = "mai2.yaml" - VER_MAIMAI = 1000 - VER_MAIMAI_PLUS = 1001 - VER_MAIMAI_GREEN = 1002 - VER_MAIMAI_GREEN_PLUS = 1003 - VER_MAIMAI_ORANGE = 1004 - VER_MAIMAI_ORANGE_PLUS = 1005 - VER_MAIMAI_PINK = 1006 - VER_MAIMAI_PINK_PLUS = 1007 - VER_MAIMAI_MURASAKI = 1008 - VER_MAIMAI_MURASAKI_PLUS = 1009 - VER_MAIMAI_MILK = 1010 - VER_MAIMAI_MILK_PLUS = 1011 - VER_MAIMAI_FINALE = 1012 + VER_MAIMAI = 0 + VER_MAIMAI_PLUS = 1 + VER_MAIMAI_GREEN = 2 + VER_MAIMAI_GREEN_PLUS = 3 + VER_MAIMAI_ORANGE = 4 + VER_MAIMAI_ORANGE_PLUS = 5 + VER_MAIMAI_PINK = 6 + VER_MAIMAI_PINK_PLUS = 7 + VER_MAIMAI_MURASAKI = 8 + VER_MAIMAI_MURASAKI_PLUS = 9 + VER_MAIMAI_MILK = 10 + VER_MAIMAI_MILK_PLUS = 11 + VER_MAIMAI_FINALE = 12 - VER_MAIMAI_DX = 0 - VER_MAIMAI_DX_PLUS = 1 - VER_MAIMAI_DX_SPLASH = 2 - VER_MAIMAI_DX_SPLASH_PLUS = 3 - VER_MAIMAI_DX_UNIVERSE = 4 - VER_MAIMAI_DX_UNIVERSE_PLUS = 5 - VER_MAIMAI_DX_FESTIVAL = 6 + VER_MAIMAI_DX = 13 + VER_MAIMAI_DX_PLUS = 14 + VER_MAIMAI_DX_SPLASH = 15 + VER_MAIMAI_DX_SPLASH_PLUS = 16 + VER_MAIMAI_DX_UNIVERSE = 17 + VER_MAIMAI_DX_UNIVERSE_PLUS = 18 + VER_MAIMAI_DX_FESTIVAL = 19 VERSION_STRING = ( - "maimai DX", - "maimai DX PLUS", - "maimai DX Splash", - "maimai DX Splash PLUS", - "maimai DX Universe", - "maimai DX Universe PLUS", - "maimai DX Festival", - ) - - VERSION_STRING_OLD = ( "maimai", "maimai PLUS", "maimai GreeN", @@ -76,11 +66,16 @@ class Mai2Constants: "maimai MURASAKi PLUS", "maimai MiLK", "maimai MiLK PLUS", - "maimai FiNALE", + "maimai FiNALE", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX Universe", + "maimai DX Universe PLUS", + "maimai DX Festival", ) @classmethod def game_ver_to_string(cls, ver: int): - if ver >= 1000: - return cls.VERSION_STRING_OLD[ver - 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9a9cae7..9d07d49 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -2,6 +2,7 @@ from typing import Any, List, Dict from datetime import datetime, timedelta import pytz import json +from random import randint from core.config import CoreConfig from titles.mai2.base import Mai2Base @@ -13,3 +14,727 @@ class Mai2DX(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX + + def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_detail(data["userId"], self.version) + o = self.data.profile.get_profile_option(data["userId"], self.version) + if p is None or o is None: + return {} # Register + profile = p._asdict() + option = o._asdict() + + return { + "userId": data["userId"], + "userName": profile["userName"], + "isLogin": False, + "lastGameId": profile["lastGameId"], + "lastDataVersion": profile["lastDataVersion"], + "lastRomVersion": profile["lastRomVersion"], + "lastLoginDate": profile["lastLoginDate"], + "lastPlayDate": profile["lastPlayDate"], + "playerRating": profile["playerRating"], + "nameplateId": 0, # Unused + "iconId": profile["iconId"], + "trophyId": 0, # Unused + "partnerId": profile["partnerId"], + "frameId": profile["frameId"], + "dispRate": option[ + "dispRate" + ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end + "totalAwake": profile["totalAwake"], + "isNetMember": profile["isNetMember"], + "dailyBonusDate": profile["dailyBonusDate"], + "headPhoneVolume": option["headPhoneVolume"], + "isInherit": False, # Not sure what this is or does?? + "banState": profile["banState"] + if profile["banState"] is not None + else 0, # New with uni+ + } + + def handle_user_login_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version) + + if profile is not None: + lastLoginDate = profile["lastLoginDate"] + loginCt = profile["playCount"] + + if "regionId" in data: + self.data.profile.put_profile_region(data["userId"], data["regionId"]) + else: + loginCt = 0 + lastLoginDate = "2017-12-05 07:00:00.0" + + return { + "returnCode": 1, + "lastLoginDate": lastLoginDate, + "loginCount": loginCt, + "consecutiveLoginCount": 0, # We don't really have a way to track this... + "loginId": loginCt, # Used with the playlog! + } + + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + playlog = data["userPlaylog"] + + self.data.score.put_playlog(user_id, playlog) + + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["upsertUserAll"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + upsert["userData"][0]["isNetMember"] = 1 + upsert["userData"][0].pop("accessCode") + self.data.profile.put_profile_detail( + user_id, self.version, upsert["userData"][0] + ) + + if "userExtend" in upsert and len(upsert["userExtend"]) > 0: + self.data.profile.put_profile_extend( + user_id, self.version, upsert["userExtend"][0] + ) + + if "userGhost" in upsert: + for ghost in upsert["userGhost"]: + self.data.profile.put_profile_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] + ) + + if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: + self.data.profile.put_profile_rating( + user_id, self.version, upsert["userRatingList"][0] + ) + + if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: + for k, v in upsert["userActivityList"][0].items(): + for act in v: + self.data.profile.put_profile_activity(user_id, act) + + if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: + for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), + ) + + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: + for char in upsert["userCharacterList"]: + self.data.item.put_character( + user_id, + char["characterId"], + char["level"], + char["awakening"], + char["useCount"], + ) + + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: + for item in upsert["userItemList"]: + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + item["isValid"], + ) + + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: + for login_bonus in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, + login_bonus["bonusId"], + login_bonus["point"], + login_bonus["isCurrent"], + login_bonus["isComplete"], + ) + + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: + for map in upsert["userMapList"]: + self.data.item.put_map( + user_id, + map["mapId"], + map["distance"], + map["isLock"], + map["isClear"], + map["isComplete"], + ) + + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: + for music in upsert["userMusicDetailList"]: + self.data.score.put_best_score(user_id, music) + + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: + for course in upsert["userCourseList"]: + self.data.score.put_course(user_id, course) + + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: + for fav in upsert["userFavoriteList"]: + self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) + + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + + def handle_user_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version) + if profile is None: + return + + profile_dict = profile._asdict() + profile_dict.pop("id") + profile_dict.pop("user") + profile_dict.pop("version") + + return {"userId": data["userId"], "userData": profile_dict} + + def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + extend = self.data.profile.get_profile_extend(data["userId"], self.version) + if extend is None: + return + + extend_dict = extend._asdict() + extend_dict.pop("id") + extend_dict.pop("user") + extend_dict.pop("version") + + return {"userId": data["userId"], "userExtend": extend_dict} + + def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = self.data.profile.get_profile_option(data["userId"], self.version) + if options is None: + return + + options_dict = options._asdict() + options_dict.pop("id") + options_dict.pop("user") + options_dict.pop("version") + + return {"userId": data["userId"], "userOption": options_dict} + + def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charges = self.data.item.get_charges(data["userId"]) + if user_charges is None: + return {"userId": data["userId"], "length": 0, "userChargeList": []} + + user_charge_list = [] + for charge in user_charges: + tmp = charge._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["purchaseDate"] = datetime.strftime( + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["validDate"] = datetime.strftime( + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + user_charge_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_charge_list), + "userChargeList": user_charge_list, + } + + def handle_get_user_item_api_request(self, data: Dict) -> Dict: + kind = int(data["nextIndex"] / 10000000000) + next_idx = int(data["nextIndex"] % 10000000000) + user_item_list = self.data.item.get_items(data["userId"], kind) + + items: list[Dict[str, Any]] = [] + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): + break + + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "itemKind": kind, + "userItemList": items, + } + + def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + chara_list.append(tmp) + + return {"userId": data["userId"], "userCharacterList": chara_list} + + def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + if favorites is None: + return + + userFavs = [] + for fav in favorites: + userFavs.append( + { + "userId": data["userId"], + "itemKind": fav["itemKind"], + "itemIdList": fav["itemIdList"], + } + ) + + return {"userId": data["userId"], "userFavoriteData": userFavs} + + def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) + if ghost is None: + return + + ghost_dict = ghost._asdict() + ghost_dict.pop("user") + ghost_dict.pop("id") + ghost_dict.pop("version_int") + + return {"userId": data["userId"], "userGhost": ghost_dict} + + def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_profile_rating(data["userId"], self.version) + if rating is None: + return + + rating_dict = rating._asdict() + rating_dict.pop("user") + rating_dict.pop("id") + rating_dict.pop("version") + + return {"userId": data["userId"], "userRating": rating_dict} + + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + """ + kind 1 is playlist, kind 2 is music list + """ + playlist = self.data.profile.get_profile_activity(data["userId"], 1) + musiclist = self.data.profile.get_profile_activity(data["userId"], 2) + if playlist is None or musiclist is None: + return + + plst = [] + mlst = [] + + for play in playlist: + tmp = play._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + plst.append(tmp) + + for music in musiclist: + tmp = music._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + mlst.append(tmp) + + return {"userActivity": {"playList": plst, "musicList": mlst}} + + def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_courses = self.data.score.get_courses(data["userId"]) + if user_courses is None: + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} + + course_list = [] + for course in user_courses: + tmp = course._asdict() + tmp.pop("user") + tmp.pop("id") + course_list.append(tmp) + + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} + + def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + # No support for custom pfps + return {"length": 0, "userPortraitList": []} + + def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } + + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: + break + + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userFriendSeasonRankingList": friend_season_ranking_list, + } + + def handle_get_user_map_api_request(self, data: Dict) -> Dict: + maps = self.data.item.get_maps(data["userId"]) + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } + + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: + break + + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userMapList": map_list, + } + + def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + login_bonuses = self.data.item.get_login_bonuses(data["userId"]) + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } + + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: + break + + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userLoginBonusList": login_bonus_list, + } + + def handle_get_user_region_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_music_api_request(self, data: Dict) -> Dict: + songs = self.data.score.get_best_scores(data["userId"]) + music_detail_list = [] + next_index = 0 + + if songs is not None: + for song in songs: + 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 + + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMusicList": [{"userMusicDetailList": music_detail_list}], + } + + 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/dxplus.py b/titles/mai2/dxplus.py index 64c9297..9062ff5 100644 --- a/titles/mai2/dxplus.py +++ b/titles/mai2/dxplus.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2DXPlus(Mai2Base): +class Mai2DXPlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 4e51619..85b3df2 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Festival(Mai2UniversePlus): +class Mai2Festival(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 4d6a177..e6818de 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -34,16 +34,6 @@ class Mai2Servlet: ) self.versions = [ - Mai2DX, - Mai2DXPlus, - Mai2Splash, - Mai2SplashPlus, - Mai2Universe, - Mai2UniversePlus, - Mai2Festival, - ] - - self.versions_old = [ Mai2Base, None, None, @@ -56,7 +46,14 @@ class Mai2Servlet: None, None, None, - Mai2Finale, + Mai2Finale, + Mai2DX, + Mai2DXPlus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival, ] self.logger = logging.getLogger("mai2") @@ -122,7 +119,7 @@ class Mai2Servlet: endpoint = url_split[len(url_split) - 1] client_ip = Utils.get_ip_addr(request) - if request.uri.startswith(b"/SDEY"): + if request.uri.startswith(b"/SDEZ"): if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX elif version >= 105 and version < 110: # Plus @@ -187,12 +184,7 @@ class Mai2Servlet: self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" - - if internal_ver >= Mai2Constants.VER_MAIMAI: - handler_cls = self.versions_old[internal_ver](self.core_cfg, self.game_cfg) - - else: - handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") @@ -213,3 +205,9 @@ class Mai2Servlet: self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + + def render_GET(self, request: Request, version: int, url_path: str) -> bytes: + if url_path.endswith("ping"): + return zlib.compress(b"ok") + else: + return zlib.compress(b"{}") \ No newline at end of file diff --git a/titles/mai2/splash.py b/titles/mai2/splash.py index ad31695..0c9f827 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Splash(Mai2Base): +class Mai2Splash(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index 54431c9..e26b267 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2SplashPlus(Mai2Base): +class Mai2SplashPlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 56b3e8f..adcb205 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -5,185 +5,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Universe(Mai2Base): +class Mai2Universe(Mai2DX): 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 e45c719..e9f03f4 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.universe import Mai2Universe +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2Universe): +class Mai2UniversePlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS From 9766e3ab78cec0e2c930d747a116cee7fab813a8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 7 May 2023 02:16:50 -0400 Subject: [PATCH 212/589] mai2: hardcode reboot time --- titles/mai2/base.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 7cfdb93..46143ec 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -23,19 +23,12 @@ class Mai2Base: self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): - # TODO: See if making this epoch 0 breaks things - reboot_start = date.strftime( - datetime.fromtimestamp(0.0), Mai2Constants.DATE_TIME_FORMAT - ) - reboot_end = date.strftime( - datetime.fromtimestamp(0.0) + timedelta(hours=1), Mai2Constants.DATE_TIME_FORMAT - ) return { "gameSetting": { "isMaintenance": "false", "requestInterval": 10, - "rebootStartTime": reboot_start, - "rebootEndTime": reboot_end, + "rebootStartTime": "2020-01-01 07:00:00.0", + "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", From d172e5582b3c88d8b526704443c0c091172eedc8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 9 May 2023 03:53:31 -0400 Subject: [PATCH 213/589] fixup allnet response for res class 2 --- core/allnet.py | 17 +++++++++-------- titles/mai2/index.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index ab435e7..31065d2 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -194,7 +194,7 @@ class AllnetServlet: self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" ) - resp = AllnetDownloadOrderResponse() + resp = AllnetDownloadOrderResponse(serial=req.serial) if ( not self.config.allnet.allow_online_updates @@ -430,6 +430,7 @@ class AllnetPowerOnResponse3: class AllnetPowerOnResponse2: def __init__(self) -> None: + time = datetime.now(tz=pytz.timezone("Asia/Tokyo")) self.stat = 1 self.uri = "" self.host = "" @@ -442,14 +443,14 @@ class AllnetPowerOnResponse2: self.region_name2 = "Y" self.region_name3 = "Z" self.country = "JPN" - self.year = datetime.now().year - self.month = datetime.now().month - self.day = datetime.now().day - self.hour = datetime.now().hour - self.minute = datetime.now().minute - self.second = datetime.now().second + self.year = time.year + self.month = time.month + self.day = time.day + self.hour = time.hour + self.minute = time.minute + self.second = time.second self.setting = "1" - self.timezone = "+0900" + self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" diff --git a/titles/mai2/index.py b/titles/mai2/index.py index e6818de..be73c36 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -99,7 +99,7 @@ class Mai2Servlet: return ( True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - f"{core_cfg.title.hostname}:{core_cfg.title.port}", + f"{core_cfg.title.hostname}", ) return ( From 42ed222095e7087f0ae65de9a6f98c9892372629 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 10 May 2023 02:31:30 -0400 Subject: [PATCH 214/589] mai2: add gamesetting urls --- titles/mai2/base.py | 8 +++++--- titles/mai2/config.py | 29 +++++++++++++++++++++++++++++ titles/mai2/dx.py | 6 ++++++ titles/mai2/finale.py | 8 ++++++++ titles/mai2/index.py | 30 +++++++++++++++++++++++++++--- 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 46143ec..ff2a0c5 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -15,6 +15,9 @@ class Mai2Base: self.version = Mai2Constants.VER_MAIMAI self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + self.can_deliver = False + self.can_usbdl = False + self.old_server = "" if self.core_config.server.is_develop and self.core_config.title.port > 0: self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" @@ -32,9 +35,9 @@ class Mai2Base: "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", - "deliverServerUri": self.old_server + "deliver", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", - "usbDlServerUri": self.old_server + "usbdl", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", "rebootInterval": 0, }, "isAouAccession": "true", @@ -49,7 +52,6 @@ class Mai2Base: def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) - print(self.version) events_lst = [] if events is None or not events: self.logger.warn("No enabled events, did you run the reader?") diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 3a20065..91cdd87 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -19,7 +19,36 @@ class Mai2ServerConfig: ) ) +class Mai2DeliverConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "enable", default=False + ) + + @property + def udbdl_enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "udbdl_enable", default=False + ) + + @property + def list_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "list_folder", default="" + ) + + @property + def list_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "content_folder", default="" + ) + class Mai2Config(dict): def __init__(self) -> None: self.server = Mai2ServerConfig(self) + self.deliver = Mai2DeliverConfig(self) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9d07d49..0ada84b 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -14,6 +14,12 @@ class Mai2DX(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX + + if self.core_config.server.is_develop and self.core_config.title.port > 0: + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEZ/100/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/" def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/finale.py b/titles/mai2/finale.py index bb4b67e..e29196f 100644 --- a/titles/mai2/finale.py +++ b/titles/mai2/finale.py @@ -13,3 +13,11 @@ class Mai2Finale(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_FINALE + self.can_deliver = True + self.can_usbdl = True + + if self.core_config.server.is_develop and self.core_config.title.port > 0: + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" diff --git a/titles/mai2/index.py b/titles/mai2/index.py index be73c36..0bb8b44 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -207,7 +207,31 @@ class Mai2Servlet: return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.endswith("ping"): - return zlib.compress(b"ok") + self.logger.info(f"v{version} GET {url_path}") + url_split = url_path.split("/") + + if url_split[0] == "old": + if url_split[1] == "ping": + self.logger.info(f"v{version} old server ping") + return zlib.compress(b"ok") + + elif url_split[1].startswith("userdata"): + self.logger.info(f"v{version} old server userdata inquire") + return zlib.compress(b"{}") + + elif url_split[1].startswith("friend"): + self.logger.info(f"v{version} old server friend inquire") + return zlib.compress(b"{}") + + elif url_split[0] == "usbdl": + if url_split[1] == "CONNECTIONTEST": + self.logger.info(f"v{version} usbdl server test") + return zlib.compress(b"ok") + + elif url_split[0] == "deliver": + if url_split[len(url_split) - 1] == "maimai_deliver.list": + self.logger.info(f"v{version} maimai_deliver.list inquire") + return zlib.compress(b"") + else: - return zlib.compress(b"{}") \ No newline at end of file + return zlib.compress(b"{}") From b85a65204fdf28ace1d91b45a53c460acf8ac569 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 10 May 2023 21:32:35 +0200 Subject: [PATCH 215/589] chuni: added SUN support, matchmaking, fixed bugs, added docs - Added CHUNITHM SUN support - Added first matchmaking support with CPU spawning and messages - Fixed wrong `next_idx` calculations - Added `startDate` to events to spawn the correct items - Fixed login bonus per version - Added information to docs --- core/data/schema/versions/SDBT_3_rollback.sql | 30 +++ core/data/schema/versions/SDBT_4_upgrade.sql | 29 +++ docs/game_specific_info.md | 75 +++++--- example_config/chuni.yaml | 3 + readme.md | 21 +- titles/chuni/__init__.py | 2 +- titles/chuni/base.py | 104 ++++++---- titles/chuni/const.py | 28 +-- titles/chuni/index.py | 28 +-- titles/chuni/new.py | 180 +++++++++++++++++- titles/chuni/newplus.py | 2 +- titles/chuni/schema/item.py | 142 +++++++++++++- titles/chuni/schema/profile.py | 27 +-- titles/chuni/schema/score.py | 4 +- titles/chuni/schema/static.py | 47 +++-- titles/chuni/sun.py | 37 ++++ titles/mai2/const.py | 6 +- 17 files changed, 626 insertions(+), 139 deletions(-) create mode 100644 core/data/schema/versions/SDBT_3_rollback.sql create mode 100644 core/data/schema/versions/SDBT_4_upgrade.sql create mode 100644 titles/chuni/sun.py diff --git a/core/data/schema/versions/SDBT_3_rollback.sql b/core/data/schema/versions/SDBT_3_rollback.sql new file mode 100644 index 0000000..ff78c54 --- /dev/null +++ b/core/data/schema/versions/SDBT_3_rollback.sql @@ -0,0 +1,30 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + DROP COLUMN regionId, + DROP COLUMN machineType; + +ALTER TABLE chuni_static_events + DROP COLUMN startDate; + +ALTER TABLE chuni_profile_data + DROP COLUMN rankUpChallengeResults; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN presetId id INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + ADD PRIMARY KEY(id); +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId) + REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_4_upgrade.sql b/core/data/schema/versions/SDBT_4_upgrade.sql new file mode 100644 index 0000000..984447e --- /dev/null +++ b/core/data/schema/versions/SDBT_4_upgrade.sql @@ -0,0 +1,29 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + ADD COLUMN regionId INT, + ADD COLUMN machineType INT; + +ALTER TABLE chuni_static_events + ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE chuni_profile_data + ADD COLUMN rankUpChallengeResults JSON; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN id presetId INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; +ALTER TABLE chuni_static_login_bonus_preset + DROP INDEX chuni_static_login_bonus_preset_uk; +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version) + REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f1b334e..694885b 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -9,7 +9,7 @@ using the megaime database. Clean installations always create the latest databas # Table of content - [Supported Games](#supported-games) - - [Chunithm](#chunithm) + - [CHUNITHM](#chunithm) - [crossbeats REV.](#crossbeats-rev) - [maimai DX](#maimai-dx) - [O.N.G.E.K.I.](#o-n-g-e-k-i) @@ -21,30 +21,31 @@ using the megaime database. Clean installations always create the latest databas Games listed below have been tested and confirmed working. -## Chunithm +## CHUNITHM ### SDBT -| Version ID | Version Name | -|------------|--------------------| -| 0 | Chunithm | -| 1 | Chunithm+ | -| 2 | Chunithm Air | -| 3 | Chunithm Air + | -| 4 | Chunithm Star | -| 5 | Chunithm Star + | -| 6 | Chunithm Amazon | -| 7 | Chunithm Amazon + | -| 8 | Chunithm Crystal | -| 9 | Chunithm Crystal + | -| 10 | Chunithm Paradise | +| Version ID | Version Name | +|------------|-----------------------| +| 0 | CHUNITHM | +| 1 | CHUNITHM PLUS | +| 2 | CHUNITHM AIR | +| 3 | CHUNITHM AIR PLUS | +| 4 | CHUNITHM STAR | +| 5 | CHUNITHM STAR PLUS | +| 6 | CHUNITHM AMAZON | +| 7 | CHUNITHM AMAZON PLUS | +| 8 | CHUNITHM CRYSTAL | +| 9 | CHUNITHM CRYSTAL PLUS | +| 10 | CHUNITHM PARADISE | ### SDHD/SDBT -| Version ID | Version Name | -|------------|-----------------| -| 11 | Chunithm New!! | -| 12 | Chunithm New!!+ | +| Version ID | Version Name | +|------------|---------------------| +| 11 | CHUNITHM NEW!! | +| 12 | CHUNITHM NEW PLUS!! | +| 13 | CHUNITHM SUN | ### Importer @@ -60,13 +61,33 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac ### Database upgrade Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see -which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to +which version is the latest, f.e. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to perform all previous updates as well: ```shell python dbutils.py --game SDBT upgrade ``` +### Online Battle + +**Only matchmaking (with your imaginary friends) is supported! Online Battle does not (yet?) work!** + +The first person to start the Online Battle (now called host) will create a "matching room" with a given `roomId`, after that max 3 other people can join the created room. +Non used slots during the matchmaking will be filled with CPUs after the timer runs out. +As soon as a new member will join the room the timer will jump back to 60 secs again. +Sending those 4 messages to all other users is also working properly. +In order to use the Online Battle every user needs the same ICF, same rom version and same data version! +If a room is full a new room will be created if another user starts an Online Battle. +After a failed Online Battle the room will be deleted. The host is used for the timer countdown, so if the connection failes to the host the timer will stop and could create a "frozen" state. + +#### Information/Problems: + +- Online Battle uses UDP hole punching and opens port 50201? +- `reflectorUri` seems related to that? +- Timer countdown should be handled globally and not by one user +- Game can freeze or can crash if someone (especially the host) leaves the matchmaking + + ## crossbeats REV. ### SDCA @@ -111,9 +132,9 @@ Config file is located in `config/cxb.yaml`. | 1 | maimai DX PLUS | | 2 | maimai DX Splash | | 3 | maimai DX Splash PLUS | -| 4 | maimai DX Universe | -| 5 | maimai DX Universe PLUS | -| 6 | maimai DX Festival | +| 4 | maimai DX UNiVERSE | +| 5 | maimai DX UNiVERSE PLUS | +| 6 | maimai DX FESTiVAL | ### Importer @@ -238,13 +259,13 @@ python dbutils.py --game SDDT upgrade ### Support status * Card Maker 1.34: - * Chunithm New!!: Yes - * maimai DX Universe: Yes + * CHUNITHM NEW!!: Yes + * maimai DX UNiVERSE: Yes * O.N.G.E.K.I. Bright: Yes * Card Maker 1.35: - * Chunithm New!!+: Yes - * maimai DX Universe PLUS: Yes + * CHUNITHM SUN: Yes (NEW PLUS!! up to A032) + * maimai DX FESTiVAL: Yes (up to A35) (UNiVERSE PLUS up to A031) * O.N.G.E.K.I. Bright Memory: Yes diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index bbac976..59db51e 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -15,6 +15,9 @@ version: 12: rom: 2.05.00 data: 2.05.00 + 13: + rom: 2.10.00 + data: 2.10.00 crypto: encrypted_only: False \ No newline at end of file diff --git a/readme.md b/readme.md index ec25191..ee407cd 100644 --- a/readme.md +++ b/readme.md @@ -3,30 +3,31 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar # Supported games Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. -+ Chunithm - + All versions up to New!! Plus -+ Crossbeats Rev ++ CHUNITHM + + All versions up to SUN + ++ crossbeats REV. + All versions + omnimix + maimai DX - + All versions up to Festival + + All versions up to FESTiVAL -+ Hatsune Miku Arcade ++ Hatsune Miku: Project DIVA Arcade + All versions + Card Maker - + 1.34.xx - + 1.35.xx + + 1.34 + + 1.35 -+ Ongeki ++ O.N.G.E.K.I. + All versions up to Bright Memory -+ Wacca ++ WACCA + Lily R + Reverse -+ Pokken ++ POKKÉN TOURNAMENT + Final Online ## Requirements diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 89cd4f5..0c3cc79 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 3 +current_schema_version = 4 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 0eaabff..689c2fe 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -44,13 +44,15 @@ class ChuniBase: # check if a user already has some pogress and if not add the # login bonus entry user_login_bonus = self.data.item.get_login_bonus( - user_id, self.version, preset["id"] + user_id, self.version, preset["presetId"] ) if user_login_bonus is None: - self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + self.data.item.put_login_bonus( + user_id, self.version, preset["presetId"] + ) # yeah i'm lazy user_login_bonus = self.data.item.get_login_bonus( - user_id, self.version, preset["id"] + user_id, self.version, preset["presetId"] ) # skip the login bonus entirely if its already finished @@ -66,13 +68,13 @@ class ChuniBase: last_update_date = datetime.now() all_login_boni = self.data.static.get_login_bonus( - self.version, preset["id"] + self.version, preset["presetId"] ) # skip the current bonus preset if no boni were found if all_login_boni is None or len(all_login_boni) < 1: self.logger.warn( - f"No bonus entries found for bonus preset {preset['id']}" + f"No bonus entries found for bonus preset {preset['presetId']}" ) continue @@ -83,14 +85,14 @@ class ChuniBase: if bonus_count > max_needed_days: # assume that all login preset ids under 3000 needs to be # looped, like 30 and 40 are looped, 40 does not work? - if preset["id"] < 3000: + if preset["presetId"] < 3000: bonus_count = 1 else: is_finished = True # grab the item for the corresponding day login_item = self.data.static.get_login_bonus_by_required_days( - self.version, preset["id"], bonus_count + self.version, preset["presetId"], bonus_count ) if login_item is not None: # now add the present to the database so the @@ -108,7 +110,7 @@ class ChuniBase: self.data.item.put_login_bonus( user_id, self.version, - preset["id"], + preset["presetId"], bonusCount=bonus_count, lastUpdateDate=last_update_date, isWatched=False, @@ -156,12 +158,18 @@ class ChuniBase: event_list = [] for evt_row in game_events: - tmp = {} - tmp["id"] = evt_row["eventId"] - tmp["type"] = evt_row["type"] - tmp["startDate"] = "2017-12-05 07:00:00.0" - tmp["endDate"] = "2099-12-31 00:00:00.0" - event_list.append(tmp) + event_list.append( + { + "id": evt_row["eventId"], + "type": evt_row["type"], + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + evt_row["startDate"], "%Y-%m-%d %H:%M:%S" + ), + "endDate": "2099-12-31 00:00:00", + } + ) return { "type": data["type"], @@ -228,29 +236,36 @@ class ChuniBase: def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) if characters is None: - return {} - next_idx = -1 + return { + "userId": data["userId"], + "length": 0, + "nextIndex": -1, + "userCharacterList": [], + } - characterList = [] - for x in range(int(data["nextIndex"]), len(characters)): + character_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(characters)): tmp = characters[x]._asdict() tmp.pop("user") tmp.pop("id") - characterList.append(tmp) + character_list.append(tmp) - if len(characterList) >= int(data["maxCount"]): + if len(character_list) >= max_ct: break - if len(characterList) >= int(data["maxCount"]) and len(characters) > int( - data["maxCount"] - ) + int(data["nextIndex"]): - next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1 + if len(characters) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = -1 return { "userId": data["userId"], - "length": len(characterList), + "length": len(character_list), "nextIndex": next_idx, - "userCharacterList": characterList, + "userCharacterList": character_list, } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: @@ -292,8 +307,8 @@ class ChuniBase: if len(user_course_list) >= max_ct: break - if len(user_course_list) >= max_ct: - next_idx = next_idx + max_ct + if len(user_course_list) >= next_idx + max_ct: + next_idx += max_ct else: next_idx = -1 @@ -347,12 +362,23 @@ class ChuniBase: } def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_fav_item_list = [] + + # still needs to be implemented on WebUI + # 1: Music, 3: Character + fav_list = self.data.item.get_all_favorites( + data["userId"], self.version, fav_kind=int(data["kind"]) + ) + if fav_list is not None: + for fav in fav_list: + user_fav_item_list.append({"id": fav["favId"]}) + return { "userId": data["userId"], - "length": 0, + "length": len(user_fav_item_list), "kind": data["kind"], "nextIndex": -1, - "userFavoriteItemList": [], + "userFavoriteItemList": user_fav_item_list, } def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: @@ -387,13 +413,13 @@ class ChuniBase: xout = kind * 10000000000 + next_idx + len(items) if len(items) < int(data["maxCount"]): - nextIndex = 0 + next_idx = 0 else: - nextIndex = xout + next_idx = xout return { "userId": data["userId"], - "nextIndex": nextIndex, + "nextIndex": next_idx, "itemKind": kind, "length": len(items), "userItemList": items, @@ -452,6 +478,7 @@ class ChuniBase: "nextIndex": -1, "userMusicList": [], # 240 } + song_list = [] next_idx = int(data["nextIndex"]) max_ct = int(data["maxCount"]) @@ -474,10 +501,10 @@ class ChuniBase: if len(song_list) >= max_ct: break - if len(song_list) >= max_ct: + if len(song_list) >= next_idx + max_ct: next_idx += max_ct else: - next_idx = 0 + next_idx = -1 return { "userId": data["userId"], @@ -623,12 +650,15 @@ class ChuniBase: self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) + if "userDataEx" in upsert: self.data.profile.put_profile_data_ex( user_id, self.version, upsert["userDataEx"][0] ) + if "userGameOption" in upsert: self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) + if "userGameOptionEx" in upsert: self.data.profile.put_profile_option_ex( user_id, upsert["userGameOptionEx"][0] @@ -672,6 +702,10 @@ class ChuniBase: if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: + # convert the player names to utf-8 + playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"]) + playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) + playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) self.data.score.put_playlog(user_id, playlog) if "userTeamPoint" in upsert: diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 6ab3cc3..b3a4cb5 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -17,21 +17,23 @@ class ChuniConstants: VER_CHUNITHM_PARADISE = 10 VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW_PLUS = 12 + VER_CHUNITHM_SUN = 13 VERSION_NAMES = [ - "Chunithm", - "Chunithm+", - "Chunithm Air", - "Chunithm Air+", - "Chunithm Star", - "Chunithm Star+", - "Chunithm Amazon", - "Chunithm Amazon+", - "Chunithm Crystal", - "Chunithm Crystal+", - "Chunithm Paradise", - "Chunithm New!!", - "Chunithm New!!+", + "CHUNITHM", + "CHUNITHM PLUS", + "CHUNITHM AIR", + "CHUNITHM AIR PLUS", + "CHUNITHM STAR", + "CHUNITHM STAR PLUS", + "CHUNITHM AMAZON", + "CHUNITHM AMAZON PLUS", + "CHUNITHM CRYSTAL", + "CHUNITHM CRYSTAL PLUS", + "CHUNITHM PARADISE", + "CHUNITHM NEW!!", + "CHUNITHM NEW PLUS!!", + "CHUNITHM SUN" ] @classmethod diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a7545ba..811840a 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -29,6 +29,7 @@ from titles.chuni.crystalplus import ChuniCrystalPlus from titles.chuni.paradise import ChuniParadise from titles.chuni.new import ChuniNew from titles.chuni.newplus import ChuniNewPlus +from titles.chuni.sun import ChuniSun class ChuniServlet: @@ -55,6 +56,7 @@ class ChuniServlet: ChuniParadise, ChuniNew, ChuniNewPlus, + ChuniSun, ] self.logger = logging.getLogger("chuni") @@ -145,30 +147,32 @@ class ChuniServlet: if version < 105: # 1.0 internal_ver = ChuniConstants.VER_CHUNITHM - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # PLUS internal_ver = ChuniConstants.VER_CHUNITHM_PLUS - elif version >= 110 and version < 115: # Air + elif version >= 110 and version < 115: # AIR internal_ver = ChuniConstants.VER_CHUNITHM_AIR - elif version >= 115 and version < 120: # Air Plus + elif version >= 115 and version < 120: # AIR PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS - elif version >= 120 and version < 125: # Star + elif version >= 120 and version < 125: # STAR internal_ver = ChuniConstants.VER_CHUNITHM_STAR - elif version >= 125 and version < 130: # Star Plus + elif version >= 125 and version < 130: # STAR PLUS internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS - elif version >= 130 and version < 135: # Amazon + elif version >= 130 and version < 135: # AMAZON internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON - elif version >= 135 and version < 140: # Amazon Plus + elif version >= 135 and version < 140: # AMAZON PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - elif version >= 140 and version < 145: # Crystal + elif version >= 140 and version < 145: # CRYSTAL internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL - elif version >= 145 and version < 150: # Crystal Plus + elif version >= 145 and version < 150: # CRYSTAL PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 150 and version < 200: # Paradise + elif version >= 150 and version < 200: # PARADISE internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 200 and version < 205: # New + elif version >= 200 and version < 205: # NEW!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 205 and version < 210: # New Plus + elif version >= 205 and version < 210: # NEW PLUS!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 210: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 67b6fcc..40dee9b 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -23,41 +23,44 @@ class ChuniNew(ChuniBase): self.version = ChuniConstants.VER_CHUNITHM_NEW def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + # use UTC time and convert it to JST time by adding +9 + # matching therefore starts one hour before and lasts for 8 hours match_start = datetime.strftime( - datetime.now() - timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=8), self.date_time_format ) match_end = datetime.strftime( - datetime.now() + timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=16), self.date_time_format ) reboot_start = datetime.strftime( - datetime.now() - timedelta(hours=11), self.date_time_format + datetime.utcnow() + timedelta(hours=6), self.date_time_format ) reboot_end = datetime.strftime( - datetime.now() - timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=7), self.date_time_format ) return { "gameSetting": { - "isMaintenance": "false", + "isMaintenance": False, "requestInterval": 10, "rebootStartTime": reboot_start, "rebootEndTime": reboot_end, - "isBackgroundDistribute": "false", + "isBackgroundDistribute": False, "maxCountCharacter": 300, "maxCountItem": 300, "maxCountMusic": 300, "matchStartTime": match_start, "matchEndTime": match_end, - "matchTimeLimit": 99, + "matchTimeLimit": 60, "matchErrorLimit": 9999, "romVersion": self.game_cfg.version.version(self.version)["rom"], "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + # might be really important for online battle to connect the cabs via UDP port 50201 "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", }, - "isDumpUpload": "false", - "isAou": "false", + "isDumpUpload": False, + "isAou": False, } def handle_remove_token_api_request(self, data: Dict) -> Dict: @@ -468,3 +471,162 @@ class ChuniNew(ChuniBase): self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} + + def handle_ping_request(self, data: Dict) -> Dict: + # matchmaking ping request + return {"returnCode": "1"} + + def handle_begin_matching_api_request(self, data: Dict) -> Dict: + room_id = 1 + # check if there is a free matching room + matching_room = self.data.item.get_oldest_free_matching(self.version) + + if matching_room is None: + # grab the latest roomId and add 1 for the new room + newest_matching = self.data.item.get_newest_matching(self.version) + if newest_matching is not None: + room_id = newest_matching["roomId"] + 1 + + # fix userName WTF8 + new_member = data["matchingMemberInfo"] + new_member["userName"] = self.read_wtf8(new_member["userName"]) + + # create the new room with room_id and the current user id (host) + # user id is required for the countdown later on + self.data.item.put_matching( + self.version, room_id, [new_member], user_id=new_member["userId"] + ) + + # get the newly created matching room + matching_room = self.data.item.get_matching(self.version, room_id) + else: + # a room already exists, so just add the new member to it + matching_member_list = matching_room["matchingMemberInfoList"] + # fix userName WTF8 + new_member = data["matchingMemberInfo"] + new_member["userName"] = self.read_wtf8(new_member["userName"]) + matching_member_list.append(new_member) + + # add the updated room to the database, make sure to set isFull correctly! + self.data.item.put_matching( + self.version, + matching_room["roomId"], + matching_member_list, + user_id=matching_room["user"], + is_full=True if len(matching_member_list) >= 4 else False, + ) + + matching_wait = { + "isFinish": False, + "restMSec": matching_room["restMSec"], # in sec + "pollingInterval": 1, # in sec + "matchingMemberInfoList": matching_room["matchingMemberInfoList"], + } + + return {"roomId": 1, "matchingWaitState": matching_wait} + + def handle_end_matching_api_request(self, data: Dict) -> Dict: + matching_room = self.data.item.get_matching(self.version, data["roomId"]) + members = matching_room["matchingMemberInfoList"] + + # only set the host user to role 1 every other to 0? + role_list = [ + {"role": 1} if m["userId"] == matching_room["user"] else {"role": 0} + for m in members + ] + + self.data.item.put_matching( + self.version, + matching_room["roomId"], + members, + user_id=matching_room["user"], + rest_sec=0, # make sure to always set 0 + is_full=True, # and full, so no one can join + ) + + return { + "matchingResult": 1, # needs to be 1 for successful matching + "matchingMemberInfoList": members, + # no idea, maybe to differentiate between CPUs and real players? + "matchingMemberRoleList": role_list, + # TCP/UDP connection? + "reflectorUri": f"{self.core_cfg.title.hostname}", + } + + def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: + # get all matching rooms, because Chuni only returns the userId + # not the actual roomId + matching_rooms = self.data.item.get_all_matchings(self.version) + if matching_rooms is None: + return {"returnCode": "1"} + + for room in matching_rooms: + old_members = room["matchingMemberInfoList"] + new_members = [m for m in old_members if m["userId"] != data["userId"]] + + # if nothing changed go to the next room + if len(old_members) == len(new_members): + continue + + # if the last user got removed, delete the matching room + if len(new_members) <= 0: + self.data.item.delete_matching(self.version, room["roomId"]) + else: + # remove the user from the room + self.data.item.put_matching( + self.version, + room["roomId"], + new_members, + user_id=room["user"], + rest_sec=room["restMSec"], + ) + + return {"returnCode": "1"} + + def handle_get_matching_state_api_request(self, data: Dict) -> Dict: + polling_interval = 1 + # get the current active room + matching_room = self.data.item.get_matching(self.version, data["roomId"]) + members = matching_room["matchingMemberInfoList"] + rest_sec = matching_room["restMSec"] + + # grab the current member + current_member = data["matchingMemberInfo"] + + # only the host user can decrease the countdown + if matching_room["user"] == int(current_member["userId"]): + # cap the restMSec to 0 + if rest_sec > 0: + rest_sec -= polling_interval + else: + rest_sec = 0 + + # update the members in order to recieve messages + for i, member in enumerate(members): + if member["userId"] == current_member["userId"]: + # replace the old user data with the current user data, + # also parse WTF-8 everytime + current_member["userName"] = self.read_wtf8(current_member["userName"]) + members[i] = current_member + + self.data.item.put_matching( + self.version, + data["roomId"], + members, + rest_sec=rest_sec, + user_id=matching_room["user"], + ) + + # only add the other members to the list + diff_members = [m for m in members if m["userId"] != current_member["userId"]] + + matching_wait = { + # makes no difference? Always use False? + "isFinish": True if rest_sec == 0 else False, + "restMSec": rest_sec, + "pollingInterval": polling_interval, + # the current user needs to be the first one? + "matchingMemberInfoList": [current_member] + diff_members, + } + + return {"matchingWaitState": matching_wait} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 4faf47a..bbe1419 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -36,6 +36,6 @@ class ChuniNewPlus(ChuniNew): def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: user_data = super().handle_cm_get_user_preview_api_request(data) - # hardcode lastDataVersion for CardMaker 1.35 + # hardcode lastDataVersion for CardMaker 1.35 A028 user_data["lastDataVersion"] = "2.05.00" return user_data diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 4ffcf93..94c4fd8 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -1,5 +1,12 @@ from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy import ( + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, + delete, +) from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.engine.base import Connection from sqlalchemy.schema import ForeignKey @@ -203,8 +210,141 @@ login_bonus = Table( mysql_charset="utf8mb4", ) +favorite = Table( + "chuni_item_favorite", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("favId", Integer, nullable=False), + Column("favKind", Integer, nullable=False, server_default="1"), + UniqueConstraint("version", "user", "favId", name="chuni_item_favorite_uk"), + mysql_charset="utf8mb4", +) + +matching = Table( + "chuni_item_matching", + metadata, + Column("roomId", Integer, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("restMSec", Integer, nullable=False, server_default="60"), + Column("isFull", Boolean, nullable=False, server_default="0"), + PrimaryKeyConstraint("roomId", "version", name="chuni_item_matching_pk"), + Column("matchingMemberInfoList", JSON, nullable=False), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def get_oldest_free_matching(self, version: int) -> Optional[Row]: + sql = matching.select( + and_( + matching.c.version == version, + matching.c.isFull == False + ) + ).order_by(matching.c.roomId.asc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_newest_matching(self, version: int) -> Optional[Row]: + sql = matching.select( + and_( + matching.c.version == version + ) + ).order_by(matching.c.roomId.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_all_matchings(self, version: int) -> Optional[List[Row]]: + sql = matching.select( + and_( + matching.c.version == version + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_matching(self, version: int, room_id: int) -> Optional[Row]: + sql = matching.select( + and_(matching.c.version == version, matching.c.roomId == room_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_matching( + self, + version: int, + room_id: int, + matching_member_info_list: list, + user_id: int = None, + rest_sec: int = 60, + is_full: bool = False + ) -> Optional[int]: + sql = insert(matching).values( + roomId=room_id, + version=version, + restMSec=rest_sec, + user=user_id, + isFull=is_full, + matchingMemberInfoList=matching_member_info_list, + ) + + conflict = sql.on_duplicate_key_update( + restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def delete_matching(self, version: int, room_id: int): + sql = delete(matching).where( + and_(matching.c.roomId == room_id, matching.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def get_all_favorites( + self, user_id: int, version: int, fav_kind: int = 1 + ) -> Optional[List[Row]]: + sql = favorite.select( + and_( + favorite.c.version == version, + favorite.c.user == user_id, + favorite.c.favKind == fav_kind, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_login_bonus( self, user_id: int, version: int, preset_id: int, **login_bonus_data ) -> Optional[int]: diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index e35769c..f8edc33 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -89,8 +89,6 @@ profile = Table( Integer, ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"), ), - Column("avatarBack", Integer, server_default="0"), - Column("avatarFace", Integer, server_default="0"), Column("eliteRankPoint", Integer, server_default="0"), Column("stockedGridCount", Integer, server_default="0"), Column("netBattleLoseCount", Integer, server_default="0"), @@ -98,10 +96,8 @@ profile = Table( Column("netBattle4thCount", Integer, server_default="0"), Column("overPowerRate", Integer, server_default="0"), Column("battleRewardStatus", Integer, server_default="0"), - Column("avatarPoint", Integer, server_default="0"), Column("netBattle1stCount", Integer, server_default="0"), Column("charaIllustId", Integer, server_default="0"), - Column("avatarItem", Integer, server_default="0"), Column("userNameEx", String(8), server_default=""), Column("netBattleWinCount", Integer, server_default="0"), Column("netBattleCorrection", Integer, server_default="0"), @@ -112,7 +108,6 @@ profile = Table( Column("netBattle3rdCount", Integer, server_default="0"), Column("netBattleConsecutiveWinCount", Integer, server_default="0"), Column("overPowerLowerRank", Integer, server_default="0"), - Column("avatarWear", Integer, server_default="0"), Column("classEmblemBase", Integer, server_default="0"), Column("battleRankPoint", Integer, server_default="0"), Column("netBattle2ndCount", Integer, server_default="0"), @@ -120,13 +115,19 @@ profile = Table( Column("skillId", Integer, server_default="0"), Column("lastCountryCode", String(5), server_default="JPN"), Column("isNetBattleHost", Boolean, server_default="0"), - Column("avatarFront", Integer, server_default="0"), - Column("avatarSkin", Integer, server_default="0"), Column("battleRewardCount", Integer, server_default="0"), Column("battleRewardIndex", Integer, server_default="0"), Column("netBattlePlayCount", Integer, server_default="0"), Column("exMapLoopCount", Integer, server_default="0"), Column("netBattleEndState", Integer, server_default="0"), + Column("rankUpChallengeResults", JSON), + Column("avatarBack", Integer, server_default="0"), + Column("avatarFace", Integer, server_default="0"), + Column("avatarPoint", Integer, server_default="0"), + Column("avatarItem", Integer, server_default="0"), + Column("avatarWear", Integer, server_default="0"), + Column("avatarFront", Integer, server_default="0"), + Column("avatarSkin", Integer, server_default="0"), Column("avatarHead", Integer, server_default="0"), UniqueConstraint("user", "version", name="chuni_profile_profile_uk"), mysql_charset="utf8mb4", @@ -417,8 +418,8 @@ class ChuniProfileData(BaseData): sql = ( select([profile, option]) .join(option, profile.c.user == option.c.user) - .filter(and_(profile.c.user == aime_id, profile.c.version == version)) - ) + .filter(and_(profile.c.user == aime_id, profile.c.version <= version)) + ).order_by(profile.c.version.desc()) result = self.execute(sql) if result is None: @@ -429,9 +430,9 @@ class ChuniProfileData(BaseData): sql = select(profile).where( and_( profile.c.user == aime_id, - profile.c.version == version, + profile.c.version <= version, ) - ) + ).order_by(profile.c.version.desc()) result = self.execute(sql) if result is None: @@ -461,9 +462,9 @@ class ChuniProfileData(BaseData): sql = select(profile_ex).where( and_( profile_ex.c.user == aime_id, - profile_ex.c.version == version, + profile_ex.c.version <= version, ) - ) + ).order_by(profile_ex.c.version.desc()) result = self.execute(sql) if result is None: diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 6a94813..203aa11 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -134,7 +134,9 @@ playlog = Table( Column("charaIllustId", Integer), Column("romVersion", String(255)), Column("judgeHeaven", Integer), - mysql_charset="utf8mb4", + Column("regionId", Integer), + Column("machineType", Integer), + mysql_charset="utf8mb4" ) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 4537518..85d0397 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -1,11 +1,19 @@ from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy import ( + ForeignKeyConstraint, + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, +) from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.engine.base import Connection from sqlalchemy.engine import Row from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from datetime import datetime from core.data.schema import BaseData, metadata @@ -17,6 +25,7 @@ events = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), mysql_charset="utf8mb4", @@ -125,11 +134,13 @@ gacha_cards = Table( login_bonus_preset = Table( "chuni_static_login_bonus_preset", metadata, - Column("id", Integer, primary_key=True, nullable=False), + Column("presetId", Integer, nullable=False), Column("version", Integer, nullable=False), Column("presetName", String(255), nullable=False), Column("isEnabled", Boolean, server_default="1"), - UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + PrimaryKeyConstraint( + "presetId", "version", name="chuni_static_login_bonus_preset_pk" + ), mysql_charset="utf8mb4", ) @@ -138,15 +149,7 @@ login_bonus = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("version", Integer, nullable=False), - Column( - "presetId", - ForeignKey( - "chuni_static_login_bonus_preset.id", - ondelete="cascade", - onupdate="cascade", - ), - nullable=False, - ), + Column("presetId", Integer, nullable=False), Column("loginBonusId", Integer, nullable=False), Column("loginBonusName", String(255), nullable=False), Column("presentId", Integer, nullable=False), @@ -157,6 +160,16 @@ login_bonus = Table( UniqueConstraint( "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" ), + ForeignKeyConstraint( + ["presetId", "version"], + [ + "chuni_static_login_bonus_preset.presetId", + "chuni_static_login_bonus_preset.version", + ], + onupdate="CASCADE", + ondelete="CASCADE", + name="chuni_static_login_bonus_ibfk_1", + ), mysql_charset="utf8mb4", ) @@ -236,7 +249,7 @@ class ChuniStaticData(BaseData): self, version: int, preset_id: int, preset_name: str, is_enabled: bool ) -> Optional[int]: sql = insert(login_bonus_preset).values( - id=preset_id, + presetId=preset_id, version=version, presetName=preset_name, isEnabled=is_enabled, @@ -416,6 +429,14 @@ class ChuniStaticData(BaseData): return None return result.fetchall() + def get_music(self, version: int) -> Optional[List[Row]]: + sql = music.select(music.c.version <= version) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py new file mode 100644 index 0000000..b56fa29 --- /dev/null +++ b/titles/chuni/sun.py @@ -0,0 +1,37 @@ +from typing import Dict, Any + +from core.config import CoreConfig +from titles.chuni.newplus import ChuniNewPlus +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig + + +class ChuniSun(ChuniNewPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_SUN + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 A032 + user_data["lastDataVersion"] = "2.10.00" + return user_data diff --git a/titles/mai2/const.py b/titles/mai2/const.py index dcc7e29..42e4349 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -37,9 +37,9 @@ class Mai2Constants: "maimai DX PLUS", "maimai DX Splash", "maimai DX Splash PLUS", - "maimai DX Universe", - "maimai DX Universe PLUS", - "maimai DX Festival", + "maimai DX UNiVERSE", + "maimai DX UNiVERSE PLUS", + "maimai DX FESTiVAL", ) @classmethod From f959236af00286b98be2666f0b7ee6f03fb97e55 Mon Sep 17 00:00:00 2001 From: Raymonf Date: Wed, 10 May 2023 17:05:11 -0400 Subject: [PATCH 216/589] SUN encryption support --- titles/chuni/index.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 811840a..5d185e9 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -98,15 +98,18 @@ class ChuniServlet: ] for method in method_list: method_fixed = inflection.camelize(method)[6:-7] + # number of iterations was changed to 70 in SUN + iter_count = 70 if version >= ChuniConstants.VER_CHUNITHM_SUN else 44 hash = PBKDF2( method_fixed, bytes.fromhex(keys[2]), 128, - count=44, + count=iter_count, hmac_hash_module=SHA1, ) - self.hash_table[version][hash.hex()] = method_fixed + hashed_name = hash.hex()[:32] # truncate unused bytes like the game does + self.hash_table[version][hashed_name] = method_fixed self.logger.debug( f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" From 0dce7e7849ac13c9b2a777caf9367697661af429 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 11 May 2023 15:33:29 +0200 Subject: [PATCH 217/589] docs: fixed opt typo --- 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 694885b..5f04f93 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -265,7 +265,7 @@ python dbutils.py --game SDDT upgrade * Card Maker 1.35: * CHUNITHM SUN: Yes (NEW PLUS!! up to A032) - * maimai DX FESTiVAL: Yes (up to A35) (UNiVERSE PLUS up to A031) + * maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031) * O.N.G.E.K.I. Bright Memory: Yes From 49166c1a7b8cbfd01e7d96dc61e28c771f8b5dd8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 11 May 2023 09:52:18 -0400 Subject: [PATCH 218/589] mai2: fix handle_get_game_setting_api_request --- titles/mai2/base.py | 16 ++++++++-------- titles/mai2/dx.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ff2a0c5..dd82fd7 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -26,21 +26,21 @@ class Mai2Base: self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): - return { + return { + "isDevelop": False, + "isAouAccession": False, "gameSetting": { - "isMaintenance": "false", - "requestInterval": 10, + "isMaintenance": False, + "requestInterval": 1800, "rebootStartTime": "2020-01-01 07:00:00.0", "rebootEndTime": "2020-01-01 07:59:59.0", - "movieUploadLimit": 10000, - "movieStatus": 0, - "movieServerUri": "", + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "movie/", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", - "rebootInterval": 0, }, - "isAouAccession": "true", } def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 0ada84b..9ac7067 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -20,6 +20,24 @@ class Mai2DX(Mai2Base): else: self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/" + + def handle_get_game_setting_api_request(self, data: Dict): + return { + "gameSetting": { + "isMaintenance": False, + "requestInterval": 1800, + "rebootStartTime": "2020-01-01 07:00:00.0", + "rebootEndTime": "2020-01-01 07:59:59.0", + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "movie/", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", + "rebootInterval": 0, + }, + "isAouAccession": False, + } def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) From 8ae0aba89cc80baed3e07e444104118d51fb2a28 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:05:05 -0400 Subject: [PATCH 219/589] mai2: update default config --- example_config/mai2.yaml | 5 +++++ titles/mai2/config.py | 8 +------- titles/mai2/index.py | 7 +++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index a04dda5..d89f5d7 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -1,3 +1,8 @@ server: enable: True loglevel: "info" + +deliver: + enable: False + udbdl_enable: False + content_folder: "" \ No newline at end of file diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 91cdd87..d5ed41f 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -36,13 +36,7 @@ class Mai2DeliverConfig: ) @property - def list_folder(self) -> int: - return CoreConfig.get_config_field( - self.__config, "mai2", "server", "list_folder", default="" - ) - - @property - def list_folder(self) -> int: + def content_folder(self) -> int: return CoreConfig.get_config_field( self.__config, "mai2", "server", "content_folder", default="" ) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 0bb8b44..c4d6874 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,4 +1,5 @@ from twisted.web.http import Request +from twisted.web.server import NOT_DONE_YET import json import inflection import yaml @@ -229,8 +230,10 @@ class Mai2Servlet: return zlib.compress(b"ok") elif url_split[0] == "deliver": - if url_split[len(url_split) - 1] == "maimai_deliver.list": - self.logger.info(f"v{version} maimai_deliver.list inquire") + file = url_split[len(url_split) - 1] + self.logger.info(f"v{version} {file} deliver inquire") + + if not self.game_cfg.deliver.enable or not path.exists(f"{self.game_cfg.deliver.content_folder}/{file}"): return zlib.compress(b"") else: From 61e3a2c9306963eef9b18a831fe3bdc6d8b43631 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:06:19 -0400 Subject: [PATCH 220/589] index: remove hanging debug log call --- index.py | 1 - 1 file changed, 1 deletion(-) diff --git a/index.py b/index.py index 7199dbe..6992ebb 100644 --- a/index.py +++ b/index.py @@ -111,7 +111,6 @@ class HttpDispatcher(resource.Resource): ) def render_GET(self, request: Request) -> bytes: - self.logger.debug(request.uri) test = self.map_get.match(request.uri.decode()) client_ip = Utils.get_ip_addr(request) From 02078080a841d981eb30169e0a2fc8694b4169a8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:12:03 -0400 Subject: [PATCH 221/589] index: additional logging for malformed return data --- index.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/index.py b/index.py index 6992ebb..c75486d 100644 --- a/index.py +++ b/index.py @@ -160,9 +160,16 @@ class HttpDispatcher(resource.Resource): if type(ret) == str: return ret.encode() - elif type(ret) == bytes: + + elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses return ret + + elif ret is None: + self.logger.warn(f"None returned by controller for {request.uri.decode()} endpoint") + return b"" + else: + self.logger.warn(f"Unknown data type returned by controller for {request.uri.decode()} endpoint") return b"" From 97892d6a7d1c10754ad0e92c91daf5028aed8661 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 18 May 2023 21:20:28 -0400 Subject: [PATCH 222/589] idz: try-catch for userdb request decryption --- titles/idz/userdb.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index 2f70ba4..2ac765e 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -83,7 +83,13 @@ class IDZUserDBProtocol(Protocol): def dataReceived(self, data: bytes) -> None: self.logger.debug(f"Receive data {data.hex()}") crypt = AES.new(self.static_key, AES.MODE_ECB) - data_dec = crypt.decrypt(data) + + try: + data_dec = crypt.decrypt(data) + + except Exception as e: + self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}") + self.logger.debug(f"Decrypt data {data_dec.hex()}") magic = struct.unpack_from(" Date: Sat, 20 May 2023 15:32:02 -0400 Subject: [PATCH 223/589] frontend: user page fixes, add card display --- core/data/schema/user.py | 3 +++ core/frontend.py | 19 ++++++++++++++++--- core/frontend/user/index.jinja | 29 ++++++++++++++++++++++++++++- core/frontend/widgets/topbar.jinja | 2 +- titles/pokken/frontend.py | 8 +++++++- titles/wacca/frontend.py | 7 ++++++- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 98663d1..6a95005 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -79,6 +79,9 @@ class UserData(BaseData): if usr["password"] is None: return False + + if passwd is None or not passwd: + return False return bcrypt.checkpw(passwd, usr["password"].encode()) diff --git a/core/frontend.py b/core/frontend.py index c992e76..9eb30e6 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -182,7 +182,7 @@ class FE_Gate(FE_Base): access_code: str = request.args[b"access_code"][0].decode() username: str = request.args[b"username"][0] email: str = request.args[b"email"][0].decode() - passwd: str = request.args[b"passwd"][0] + passwd: bytes = request.args[b"passwd"][0] uid = self.data.card.get_user_id_from_card(access_code) if uid is None: @@ -197,7 +197,7 @@ class FE_Gate(FE_Base): if result is None: return redirectTo(b"/gate?e=3", request) - if not self.data.user.check_password(uid, passwd.encode()): + if not self.data.user.check_password(uid, passwd): return redirectTo(b"/gate", request) return redirectTo(b"/user", request) @@ -227,9 +227,22 @@ class FE_User(FE_Base): usr_sesh = IUserSession(sesh) if usr_sesh.userId == 0: return redirectTo(b"/gate", request) + + cards = self.data.card.get_user_cards(usr_sesh.userId) + user = self.data.user.get_user(usr_sesh.userId) + card_data = [] + for c in cards: + if c['is_locked']: + status = 'Locked' + elif c['is_banned']: + status = 'Banned' + else: + status = 'Active' + + card_data.append({'access_code': c['access_code'], 'status': status}) return template.render( - title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh) + title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username'] ).encode("utf-16") diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja index eabdd18..2911e67 100644 --- a/core/frontend/user/index.jinja +++ b/core/frontend/user/index.jinja @@ -1,4 +1,31 @@ {% extends "core/frontend/index.jinja" %} {% block content %} -

testing

+

Management for {{ username }}

+

Cards

+
    +{% for c in cards %} +
  • {{ c.access_code }}: {{ c.status }}
  • +{% endfor %} +
+ + {% endblock content %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja index d196361..fb63ebe 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/frontend/widgets/topbar.jinja @@ -4,7 +4,7 @@
  {% for game in game_list %} -   +   {% endfor %}
diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py index e4e8947..af344dc 100644 --- a/titles/pokken/frontend.py +++ b/titles/pokken/frontend.py @@ -2,8 +2,9 @@ import yaml import jinja2 from twisted.web.http import Request from os import path +from twisted.web.server import Session -from core.frontend import FE_Base +from core.frontend import FE_Base, IUserSession from core.config import CoreConfig from .database import PokkenData from .config import PokkenConfig @@ -27,7 +28,12 @@ class PokkenFrontend(FE_Base): template = self.environment.get_template( "titles/pokken/frontend/pokken_index.jinja" ) + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + return template.render( title=f"{self.core_config.server.name} | {self.nav_name}", game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) ).encode("utf-16") diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index 69ab1ee..cc40644 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -2,8 +2,9 @@ import yaml import jinja2 from twisted.web.http import Request from os import path +from twisted.web.server import Session -from core.frontend import FE_Base +from core.frontend import FE_Base, IUserSession from core.config import CoreConfig from titles.wacca.database import WaccaData from titles.wacca.config import WaccaConfig @@ -27,7 +28,11 @@ class WaccaFrontend(FE_Base): template = self.environment.get_template( "titles/wacca/frontend/wacca_index.jinja" ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + return template.render( title=f"{self.core_config.server.name} | {self.nav_name}", game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) ).encode("utf-16") From 5ddfb88182d0b25275ecb8d99621bd4a75a2696f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 22 May 2023 12:24:16 -0400 Subject: [PATCH 224/589] wacca: fix user/music/unlock error when using tickets. --- titles/wacca/handlers/user_music.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index a8c80bf..26c2167 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -93,13 +93,10 @@ class UserMusicUnlockRequest(BaseRequest): class UserMusicUnlockResponse(BaseResponse): - def __init__(self, current_wp: int = 0, tickets_remaining: List = []) -> None: + def __init__(self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []) -> None: super().__init__() self.wp = current_wp - self.tickets: List[TicketItem] = [] - - for ticket in tickets_remaining: - self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) + self.tickets = tickets_remaining def make(self) -> Dict: tickets = [] From b9fd4f294d0f5662132a69ce16eb731125543be7 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 22 May 2023 12:33:43 -0400 Subject: [PATCH 225/589] wacca: fix type mismatch in user/music/unlock --- titles/wacca/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index ada40c6..eeafe4c 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -624,10 +624,10 @@ class WaccaBase: current_wp = profile["wp"] tickets = self.data.item.get_tickets(user_id) - new_tickets = [] + new_tickets: List[TicketItem] = [] for ticket in tickets: - new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) + new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)) for item in req.itemsUsed: if ( @@ -645,11 +645,11 @@ class WaccaBase: and not self.game_config.mods.infinite_tickets ): for x in range(len(new_tickets)): - if new_tickets[x][1] == item.itemId: + if new_tickets[x].ticketId == item.itemId: self.logger.debug( - f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}" + f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}" ) - self.data.item.spend_ticket(new_tickets[x][0]) + self.data.item.spend_ticket(new_tickets[x].userTicketId) new_tickets.pop(x) break From 7ed294e9f7f4235004879e79905472bc8b1b0ec1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 24 May 2023 01:08:53 -0400 Subject: [PATCH 226/589] delivery: remove period from version --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index ab435e7..d54b333 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -204,12 +204,12 @@ class AllnetServlet: else: # TODO: Keychip check if path.exists( - f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" ): resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists( - f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" ): resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" From 72594fef315a10aa089aca9a20c4e43a4949abf6 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 26 May 2023 13:45:20 -0400 Subject: [PATCH 227/589] adding partial Sword Art Online Arcade support --- example_config/sao.yaml | 6 + titles/sao/__init__.py | 10 + titles/sao/base.py | 226 ++++ titles/sao/config.py | 47 + titles/sao/const.py | 15 + titles/sao/database.py | 12 + titles/sao/handlers/__init__.py | 1 + titles/sao/handlers/base.py | 1698 +++++++++++++++++++++++++++++++ titles/sao/index.py | 117 +++ titles/sao/read.py | 230 +++++ titles/sao/schema/__init__.py | 2 + titles/sao/schema/profile.py | 48 + titles/sao/schema/static.py | 297 ++++++ 13 files changed, 2709 insertions(+) create mode 100644 example_config/sao.yaml create mode 100644 titles/sao/__init__.py create mode 100644 titles/sao/base.py create mode 100644 titles/sao/config.py create mode 100644 titles/sao/const.py create mode 100644 titles/sao/database.py create mode 100644 titles/sao/handlers/__init__.py create mode 100644 titles/sao/handlers/base.py create mode 100644 titles/sao/index.py create mode 100644 titles/sao/read.py create mode 100644 titles/sao/schema/__init__.py create mode 100644 titles/sao/schema/profile.py create mode 100644 titles/sao/schema/static.py diff --git a/example_config/sao.yaml b/example_config/sao.yaml new file mode 100644 index 0000000..7e3ecf2 --- /dev/null +++ b/example_config/sao.yaml @@ -0,0 +1,6 @@ +server: + hostname: "localhost" + enable: True + loglevel: "info" + port: 9000 + auto_register: True \ No newline at end of file diff --git a/titles/sao/__init__.py b/titles/sao/__init__.py new file mode 100644 index 0000000..15a46f9 --- /dev/null +++ b/titles/sao/__init__.py @@ -0,0 +1,10 @@ +from .index import SaoServlet +from .const import SaoConstants +from .database import SaoData +from .read import SaoReader + +index = SaoServlet +database = SaoData +reader = SaoReader +game_codes = [SaoConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/sao/base.py b/titles/sao/base.py new file mode 100644 index 0000000..67a2d6b --- /dev/null +++ b/titles/sao/base.py @@ -0,0 +1,226 @@ +from datetime import datetime, timedelta +import json, logging +from typing import Any, Dict +import random +import struct + +from core.data import Data +from core import CoreConfig +from .config import SaoConfig +from .database import SaoData +from titles.sao.handlers.base import * + +class SaoBase: + def __init__(self, core_cfg: CoreConfig, game_cfg: SaoConfig) -> None: + self.core_cfg = core_cfg + self.game_cfg = game_cfg + self.core_data = Data(core_cfg) + self.game_data = SaoData(core_cfg) + self.version = 0 + self.logger = logging.getLogger("sao") + + def handle_noop(self, request: Any) -> bytes: + sao_request = request + + sao_id = int(sao_request[:4],16) + 1 + + ret = struct.pack("!HHIIIIIIb", sao_id, 0, 0, 5, 1, 1, 5, 0x01000000, 0).hex() + return bytes.fromhex(ret) + + def handle_c122(self, request: Any) -> bytes: + #common/get_maintenance_info + + resp = SaoGetMaintResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c12e(self, request: Any) -> bytes: + #common/ac_cabinet_boot_notification + resp = SaoCommonAcCabinetBootNotificationResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c100(self, request: Any) -> bytes: + #common/get_app_versions + resp = SaoCommonGetAppVersionsRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c102(self, request: Any) -> bytes: + #common/master_data_version_check + resp = SaoMasterDataVersionCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c10a(self, request: Any) -> bytes: + #common/paying_play_start + resp = SaoCommonPayingPlayStartRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_ca02(self, request: Any) -> bytes: + #quest_multi_play_room/get_quest_scene_multi_play_photon_server + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c11e(self, request: Any) -> bytes: + #common/get_auth_card_data + + #Check authentication + access_code = bytes.fromhex(request[188:268]).decode("utf-16le") + user_id = self.core_data.card.get_user_id_from_card( access_code ) + + if not user_id: + user_id = self.core_data.user.create_user() #works + card_id = self.core_data.card.create_card(user_id, access_code) + + if card_id is None: + user_id = -1 + self.logger.error("Failed to register card!") + + profile_id = self.game_data.profile.create_profile(user_id) + + self.logger.info(f"User Authenticated: { access_code } | { user_id }") + + #Grab values from profile + profile_data = self.game_data.profile.get_profile(user_id) + + if user_id and not profile_data: + profile_id = self.game_data.profile.create_profile(user_id) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c40c(self, request: Any) -> bytes: + #home/check_ac_login_bonus + resp = SaoHomeCheckAcLoginBonusResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c104(self, request: Any) -> bytes: + #common/login + access_code = bytes.fromhex(request[228:308]).decode("utf-16le") + user_id = self.core_data.card.get_user_id_from_card( access_code ) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoCommonLoginResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c404(self, request: Any) -> bytes: + #home/check_comeback_event + resp = SaoCheckComebackEventRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c000(self, request: Any) -> bytes: + #ticket/ticket + resp = SaoTicketResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c500(self, request: Any) -> bytes: + #user_info/get_user_basic_data + user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c600(self, request: Any) -> bytes: + #have_object/get_hero_log_user_data_list + heroIdsData = self.game_data.static.get_hero_ids(0, True) + + resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, heroIdsData) + return resp.make() + + def handle_c602(self, request: Any) -> bytes: + #have_object/get_equipment_user_data_list + equipmentIdsData = self.game_data.static.get_equipment_ids(0, True) + + resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipmentIdsData) + return resp.make() + + def handle_c604(self, request: Any) -> bytes: + #have_object/get_item_user_data_list + itemIdsData = self.game_data.static.get_item_ids(0, True) + + resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, itemIdsData) + return resp.make() + + def handle_c606(self, request: Any) -> bytes: + #have_object/get_support_log_user_data_list + supportIdsData = self.game_data.static.get_support_log_ids(0, True) + + resp = SaoGetSupportLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, supportIdsData) + return resp.make() + + def handle_c800(self, request: Any) -> bytes: + #custom/get_title_user_data_list + titleIdsData = self.game_data.static.get_title_ids(0, True) + + resp = SaoGetTitleUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, titleIdsData) + return resp.make() + + def handle_c608(self, request: Any) -> bytes: + #have_object/get_episode_append_data_list + user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c804(self, request: Any) -> bytes: + #custom/get_party_data_list + resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c902(self, request: Any) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + #quest/get_quest_scene_prev_scan_profile_card + resp = SaoGetQuestScenePrevScanProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c124(self, request: Any) -> bytes: + #common/get_resource_path_info + resp = SaoGetResourcePathInfoResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c900(self, request: Any) -> bytes: + #quest/get_quest_scene_user_data_list // QuestScene.csv + questIdsData = self.game_data.static.get_quests_ids(0, True) + resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, questIdsData) + return resp.make() + + def handle_c400(self, request: Any) -> bytes: + #home/check_yui_medal_get_condition + resp = SaoCheckYuiMedalGetConditionResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c402(self, request: Any) -> bytes: + #home/get_yui_medal_bonus_user_data + resp = SaoGetYuiMedalBonusUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c40a(self, request: Any) -> bytes: + #home/check_profile_card_used_reward + resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c904(self, request: Any) -> bytes: + #quest/episode_play_start + user_id = bytes.fromhex(request[100:124]).decode("utf-16le") + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c908(self, request: Any) -> bytes: # function not working yet, tired of this + #quest/episode_play_end + resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c914(self, request: Any) -> bytes: + #quest/trial_tower_play_start + user_id = bytes.fromhex(request[100:124]).decode("utf-16le") + floor_id = int(request[130:132], 16) # not required but nice to know + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c90a(self, request: Any) -> bytes: #should be tweaked for proper item unlock + #quest/episode_play_end_unanalyzed_log_fixed + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() diff --git a/titles/sao/config.py b/titles/sao/config.py new file mode 100644 index 0000000..7b9a2d5 --- /dev/null +++ b/titles/sao/config.py @@ -0,0 +1,47 @@ +from core.config import CoreConfig + + +class SaoServerConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "hostname", default="localhost" + ) + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "sao", "server", "loglevel", default="info" + ) + ) + + @property + def port(self) -> int: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "port", default=9000 + ) + + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with sao + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "sao", "server", "auto_register", default=True + ) + + +class SaoConfig(dict): + def __init__(self) -> None: + self.server = SaoServerConfig(self) diff --git a/titles/sao/const.py b/titles/sao/const.py new file mode 100644 index 0000000..8bdea0f --- /dev/null +++ b/titles/sao/const.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class SaoConstants: + GAME_CODE = "SDEW" + + CONFIG_NAME = "sao.yaml" + + VER_SAO = 0 + + VERSION_NAMES = ("Sword Art Online Arcade") + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/sao/database.py b/titles/sao/database.py new file mode 100644 index 0000000..463440d --- /dev/null +++ b/titles/sao/database.py @@ -0,0 +1,12 @@ +from core.data import Data +from core.config import CoreConfig + +from .schema import * + + +class SaoData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.profile = SaoProfileData(cfg, self.session) + self.static = SaoStaticData(cfg, self.session) \ No newline at end of file diff --git a/titles/sao/handlers/__init__.py b/titles/sao/handlers/__init__.py new file mode 100644 index 0000000..90a6b4e --- /dev/null +++ b/titles/sao/handlers/__init__.py @@ -0,0 +1 @@ +from titles.sao.handlers.base import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py new file mode 100644 index 0000000..9048517 --- /dev/null +++ b/titles/sao/handlers/base.py @@ -0,0 +1,1698 @@ +import struct +from datetime import datetime +from construct import * +import sys + +class SaoBaseRequest: + def __init__(self, data: bytes) -> None: + self.cmd = struct.unpack_from("!H", bytes)[0] + # TODO: The rest of the request header + +class SaoBaseResponse: + def __init__(self, cmd_id: int) -> None: + self.cmd = cmd_id + self.err_status = 0 + self.error_type = 0 + self.vendor_id = 5 + self.game_id = 1 + self.version_id = 1 + self.length = 1 + + def make(self) -> bytes: + return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) + +class SaoNoopResponse(SaoBaseResponse): + def __init__(self, cmd: int) -> None: + super().__init__(cmd) + self.result = 1 + self.length = 5 + + def make(self) -> bytes: + return super().make() + struct.pack("!bI", self.result, 0) + +class SaoGetMaintRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + # TODO: The rest of the mait info request + +class SaoGetMaintResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.maint_begin = datetime.fromtimestamp(0) + self.maint_begin_int_ct = 6 + self.maint_end = datetime.fromtimestamp(0) + self.maint_end_int_ct = 6 + self.dt_format = "%Y%m%d%H%M%S" + + def make(self) -> bytes: + maint_begin_list = [x for x in datetime.strftime(self.maint_begin, self.dt_format)] + maint_end_list = [x for x in datetime.strftime(self.maint_end, self.dt_format)] + self.maint_begin_int_ct = len(maint_begin_list) * 2 + self.maint_end_int_ct = len(maint_end_list) * 2 + + maint_begin_bytes = b"" + maint_end_bytes = b"" + + for x in maint_begin_list: + maint_begin_bytes += struct.pack(" None: + super().__init__(data) + +class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoMasterDataVersionCheckRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoMasterDataVersionCheckResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.update_flag = 0 + self.data_version = 100 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "update_flag" / Int8ul, # result is either 0 or 1 + "data_version" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + update_flag=self.update_flag, + data_version=self.data_version, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonGetAppVersionsRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonGetAppVersionsRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_list_size = 1 # Number of arrays + + self.version_app_id = 1 + self.applying_start_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_list_size" / Int32ub, + + "version_app_id" / Int32ub, + "applying_start_date_size" / Int32ub, # big endian + "applying_start_date" / Int16ul[len(self.applying_start_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_list_size=self.data_list_size, + + version_app_id=self.version_app_id, + applying_start_date_size=len(self.applying_start_date) * 2, + applying_start_date=[ord(x) for x in self.applying_start_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonPayingPlayStartRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonPayingPlayStartRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.paying_session_id = "1" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "paying_session_id_size" / Int32ub, # big endian + "paying_session_id" / Int16ul[len(self.paying_session_id)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + paying_session_id_size=len(self.paying_session_id) * 2, + paying_session_id=[ord(x) for x in self.paying_session_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetAuthCardDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + + self.result = 1 + self.unused_card_flag = "" + self.first_play_flag = 0 + self.tutorial_complete_flag = 1 + self.nick_name = profile_data['nick_name'] # nick_name field #4 + self.personal_id = str(profile_data['user']) + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "unused_card_flag_size" / Int32ub, # big endian + "unused_card_flag" / Int16ul[len(self.unused_card_flag)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "tutorial_complete_flag" / Int8ul, # result is either 0 or 1 + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "personal_id_size" / Int32ub, # big endian + "personal_id" / Int16ul[len(self.personal_id)] + ) + + resp_data = resp_struct.build(dict( + result=self.result, + unused_card_flag_size=len(self.unused_card_flag) * 2, + unused_card_flag=[ord(x) for x in self.unused_card_flag], + first_play_flag=self.first_play_flag, + tutorial_complete_flag=self.tutorial_complete_flag, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + personal_id_size=len(self.personal_id) * 2, + personal_id=[ord(x) for x in self.personal_id] + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoHomeCheckAcLoginBonusRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.reward_get_flag = 1 + self.get_ac_login_bonus_id_list_size = 2 # Array + + self.get_ac_login_bonus_id_1 = 1 # "2020年7月9日~(アニメ&リコリス記念)" + self.get_ac_login_bonus_id_2 = 2 # "2020年10月6日~(秋のデビュー&カムバックCP)" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "reward_get_flag" / Int8ul, # result is either 0 or 1 + "get_ac_login_bonus_id_list_size" / Int32ub, + + "get_ac_login_bonus_id_1" / Int32ub, + "get_ac_login_bonus_id_2" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + reward_get_flag=self.reward_get_flag, + get_ac_login_bonus_id_list_size=self.get_ac_login_bonus_id_list_size, + + get_ac_login_bonus_id_1=self.get_ac_login_bonus_id_1, + get_ac_login_bonus_id_2=self.get_ac_login_bonus_id_2, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestSceneMultiPlayPhotonServerRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.application_id = "7df3a2f6-d69d-4073-aafe-810ee61e1cea" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "application_id_size" / Int32ub, # big endian + "application_id" / Int16ul[len(self.application_id)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + application_id_size=len(self.application_id) * 2, + application_id=[ord(x) for x in self.application_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoTicketRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoTicketResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = "1" + self.ticket_id = "9" #up to 18 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result_size" / Int32ub, # big endian + "result" / Int16ul[len(self.result)], + "ticket_id_size" / Int32ub, # big endian + "ticket_id" / Int16ul[len(self.result)], + ) + + resp_data = resp_struct.build(dict( + result_size=len(self.result) * 2, + result=[ord(x) for x in self.result], + ticket_id_size=len(self.ticket_id) * 2, + ticket_id=[ord(x) for x in self.ticket_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonLoginRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonLoginResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.user_id = str(profile_data['user']) + self.first_play_flag = 0 + self.grantable_free_ticket_flag = 1 + self.login_reward_vp = 99 + self.today_paying_flag = 1 + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[len(self.user_id)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "grantable_free_ticket_flag" / Int8ul, # result is either 0 or 1 + "login_reward_vp" / Int16ub, + "today_paying_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_id_size=len(self.user_id) * 2, + user_id=[ord(x) for x in self.user_id], + first_play_flag=self.first_play_flag, + grantable_free_ticket_flag=self.grantable_free_ticket_flag, + login_reward_vp=self.login_reward_vp, + today_paying_flag=self.today_paying_flag, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckComebackEventRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckComebackEventRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag_ = 1 + self.get_comeback_event_id_list = "" # Array of events apparently + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag_" / Int8ul, # result is either 0 or 1 + "get_comeback_event_id_list_size" / Int32ub, # big endian + "get_comeback_event_id_list" / Int16ul[len(self.get_comeback_event_id_list)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag_=self.get_flag_, + get_comeback_event_id_list_size=len(self.get_comeback_event_id_list) * 2, + get_comeback_event_id_list=[ord(x) for x in self.get_comeback_event_id_list], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetUserBasicDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetUserBasicDataResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.user_basic_data_size = 1 # Number of arrays + self.user_type = profile_data['user_type'] + self.nick_name = profile_data['nick_name'] + self.rank_num = profile_data['rank_num'] + self.rank_exp = profile_data['rank_exp'] + self.own_col = profile_data['own_col'] + self.own_vp = profile_data['own_vp'] + self.own_yui_medal = profile_data['own_yui_medal'] + self.setting_title_id = profile_data['setting_title_id'] + self.favorite_user_hero_log_id = "" + self.favorite_user_support_log_id = "" + self.my_store_id = "1" + self.my_store_name = "ARTEMiS" + self.user_reg_date = "20230101120000" + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_basic_data_size" / Int32ub, + + "user_type" / Int16ub, + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, + "rank_exp" / Int32ub, + "own_col" / Int32ub, + "own_vp" / Int32ub, + "own_yui_medal" / Int32ub, + "setting_title_id" / Int32ub, + "favorite_user_hero_log_id_size" / Int32ub, # big endian + "favorite_user_hero_log_id" / Int16ul[len(str(self.favorite_user_hero_log_id))], + "favorite_user_support_log_id_size" / Int32ub, # big endian + "favorite_user_support_log_id" / Int16ul[len(str(self.favorite_user_support_log_id))], + "my_store_id_size" / Int32ub, # big endian + "my_store_id" / Int16ul[len(str(self.my_store_id))], + "my_store_name_size" / Int32ub, # big endian + "my_store_name" / Int16ul[len(str(self.my_store_name))], + "user_reg_date_size" / Int32ub, # big endian + "user_reg_date" / Int16ul[len(self.user_reg_date)] + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_basic_data_size=self.user_basic_data_size, + + user_type=self.user_type, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=self.rank_num, + rank_exp=self.rank_exp, + own_col=self.own_col, + own_vp=self.own_vp, + own_yui_medal=self.own_yui_medal, + setting_title_id=self.setting_title_id, + favorite_user_hero_log_id_size=len(self.favorite_user_hero_log_id) * 2, + favorite_user_hero_log_id=[ord(x) for x in str(self.favorite_user_hero_log_id)], + favorite_user_support_log_id_size=len(self.favorite_user_support_log_id) * 2, + favorite_user_support_log_id=[ord(x) for x in str(self.favorite_user_support_log_id)], + my_store_id_size=len(self.my_store_id) * 2, + my_store_id=[ord(x) for x in str(self.my_store_id)], + my_store_name_size=len(self.my_store_name) * 2, + my_store_name=[ord(x) for x in str(self.my_store_name)], + user_reg_date_size=len(self.user_reg_date) * 2, + user_reg_date=[ord(x) for x in self.user_reg_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, heroIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + #print(heroIdsData) + #print(list(map(str,heroIdsData))) + + # hero_log_user_data_list + self.user_hero_log_id = list(map(str,heroIdsData)) #str + self.hero_log_id = heroIdsData #int + self.log_level = 10 #short + self.max_log_level_extended_num = 10 #short + self.log_exp = 1000 #int + self.possible_awakening_flag = 0 #byte + self.awakening_stage = 0 #short + self.awakening_exp = 0 #int + self.skill_slot_correction_value = 0 #byte + self.last_set_skill_slot1_skill_id = 0 #short + self.last_set_skill_slot2_skill_id = 0 #short + self.last_set_skill_slot3_skill_id = 0 #short + self.last_set_skill_slot4_skill_id = 0 #short + self.last_set_skill_slot5_skill_id = 0 #short + self.property1_property_id = 0 #int + self.property1_value1 = 0 #int + self.property1_value2 = 0 #int + self.property2_property_id = 0 #int + self.property2_value1 = 0 #int + self.property2_value2 = 0 #int + self.property3_property_id = 0 #int + self.property3_value1 = 0 #int + self.property3_value2 = 0 #int + self.property4_property_id = 0 #int + self.property4_value1 = 0 #int + self.property4_value2 = 0 #int + self.converted_card_num = 0 #short + self.shop_purchase_flag = 1 #byte + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + #new stuff + + hero_log_user_data_list_struct = Struct( + "user_hero_log_id_size" / Int32ub, # big endian + "user_hero_log_id" / Int16ul[9], #string + "hero_log_id" / Int32ub, #int + "log_level" / Int16ub, #short + "max_log_level_extended_num" / Int16ub, #short + "log_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 + "last_set_skill_slot1_skill_id" / Int16ub, #short + "last_set_skill_slot2_skill_id" / Int16ub, #short + "last_set_skill_slot3_skill_id" / Int16ub, #short + "last_set_skill_slot4_skill_id" / Int16ub, #short + "last_set_skill_slot5_skill_id" / Int16ub, #short + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian + "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + hero_log_user_data_list_size=0, + hero_log_user_data_list=[], + ))) + + for i in range(len(self.hero_log_id)): + hero_data = dict( + user_hero_log_id_size=len(self.user_hero_log_id[i]) * 2, + user_hero_log_id=[ord(x) for x in self.user_hero_log_id[i]], + hero_log_id=self.hero_log_id[i], + log_level=self.log_level, + max_log_level_extended_num=self.max_log_level_extended_num, + log_exp=self.log_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + skill_slot_correction_value=self.skill_slot_correction_value, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.hero_log_user_data_list.append(hero_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetEquipmentUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, equipmentIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # equipment_user_data_list + self.user_equipment_id = list(map(str,equipmentIdsData)) #str + self.equipment_id = equipmentIdsData #int + self.enhancement_value = 10 #short + self.max_enhancement_value_extended_num = 10 #short + self.enhancement_exp = 1000 #int + self.possible_awakening_flag = 0 #byte + self.awakening_stage = 0 #short + self.awakening_exp = 0 #int + self.property1_property_id = 0 #int + self.property1_value1 = 0 #int + self.property1_value2 = 0 #int + self.property2_property_id = 0 #int + self.property2_value1 = 0 #int + self.property2_value2 = 0 #int + self.property3_property_id = 0 #int + self.property3_value1 = 0 #int + self.property3_value2 = 0 #int + self.property4_property_id = 0 #int + self.property4_value1 = 0 #int + self.property4_value2 = 0 #int + self.converted_card_num = 1 #short + self.shop_purchase_flag = 1 #byte + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + + equipment_user_data_list_struct = Struct( + "user_equipment_id_size" / Int32ub, # big endian + "user_equipment_id" / Int16ul[9], #string + "equipment_id" / Int32ub, #int + "enhancement_value" / Int16ub, #short + "max_enhancement_value_extended_num" / Int16ub, #short + "enhancement_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "equipment_user_data_list_size" / Rebuild(Int32ub, len_(this.equipment_user_data_list)), # big endian + "equipment_user_data_list" / Array(this.equipment_user_data_list_size, equipment_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + equipment_user_data_list_size=0, + equipment_user_data_list=[], + ))) + + for i in range(len(self.equipment_id)): + equipment_data = dict( + user_equipment_id_size=len(self.user_equipment_id[i]) * 2, + user_equipment_id=[ord(x) for x in self.user_equipment_id[i]], + equipment_id=self.equipment_id[i], + enhancement_value=self.enhancement_value, + max_enhancement_value_extended_num=self.max_enhancement_value_extended_num, + enhancement_exp=self.enhancement_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.equipment_user_data_list.append(equipment_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetItemUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetItemUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, itemIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # item_user_data_list + self.user_item_id = list(map(str,itemIdsData)) #str + self.item_id = itemIdsData #int + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + #new stuff + + item_user_data_list_struct = Struct( + "user_item_id_size" / Int32ub, # big endian + "user_item_id" / Int16ul[6], #string but this will not work with 10000 IDs... only with 6 digits + "item_id" / Int32ub, #int + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "item_user_data_list_size" / Rebuild(Int32ub, len_(this.item_user_data_list)), # big endian + "item_user_data_list" / Array(this.item_user_data_list_size, item_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + item_user_data_list_size=0, + item_user_data_list=[], + ))) + + for i in range(len(self.item_id)): + item_data = dict( + user_item_id_size=len(self.user_item_id[i]) * 2, + user_item_id=[ord(x) for x in self.user_item_id[i]], + item_id=self.item_id[i], + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.item_user_data_list.append(item_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetSupportLogUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, supportIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # support_log_user_data_list + self.user_support_log_id = list(map(str,supportIdsData)) #str + self.support_log_id = supportIdsData #int + self.possible_awakening_flag = 0 + self.awakening_stage = 0 + self.awakening_exp = 0 + self.converted_card_num = 0 + self.shop_purchase_flag = 0 + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + support_log_user_data_list_struct = Struct( + "user_support_log_id_size" / Int32ub, # big endian + "user_support_log_id" / Int16ul[9], + "support_log_id" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, # int + "converted_card_num" / Int16ub, #short + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "support_log_user_data_list_size" / Rebuild(Int32ub, len_(this.support_log_user_data_list)), # big endian + "support_log_user_data_list" / Array(this.support_log_user_data_list_size, support_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + support_log_user_data_list_size=0, + support_log_user_data_list=[], + ))) + + for i in range(len(self.support_log_id)): + support_data = dict( + user_support_log_id_size=len(self.user_support_log_id[i]) * 2, + user_support_log_id=[ord(x) for x in self.user_support_log_id[i]], + support_log_id=self.support_log_id[i], + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.support_log_user_data_list.append(support_data) + + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetTitleUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetTitleUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, titleIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # title_user_data_list + self.user_title_id = list(map(str,titleIdsData)) #str + self.title_id = titleIdsData #int + + def make(self) -> bytes: + title_user_data_list_struct = Struct( + "user_title_id_size" / Int32ub, # big endian + "user_title_id" / Int16ul[6], #string + "title_id" / Int32ub, #int + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "title_user_data_list_size" / Rebuild(Int32ub, len_(this.title_user_data_list)), # big endian + "title_user_data_list" / Array(this.title_user_data_list_size, title_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + title_user_data_list_size=0, + title_user_data_list=[], + ))) + + for i in range(len(self.title_id)): + title_data = dict( + user_title_id_size=len(self.user_title_id[i]) * 2, + user_title_id=[ord(x) for x in self.user_title_id[i]], + title_id=self.title_id[i], + ) + + resp_data.title_user_data_list.append(title_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetEpisodeAppendDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.length = None + self.result = 1 + + self.user_episode_append_id_list = ["10001", "10002", "10003", "10004", "10005"] + self.user_id_list = [str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"])] + self.episode_append_id_list = [10001, 10002, 10003, 10004, 10005] + self.own_num_list = [3, 3, 3, 3 ,3] + + def make(self) -> bytes: + episode_data_struct = Struct( + "user_episode_append_id_size" / Int32ub, # big endian + "user_episode_append_id" / Int16ul[5], #forced to match the user_episode_append_id_list index which is always 5 chars for the episode ids + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[6], # has to be exactly 6 chars in the user field... MANDATORY + "episode_append_id" / Int32ub, + "own_num" / Int32ub, + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "episode_append_data_list_size" / Rebuild(Int32ub, len_(this.episode_append_data_list)), # big endian + "episode_append_data_list" / Array(this.episode_append_data_list_size, episode_data_struct), + ) + + # really dump to parse the build resp, but that creates a new object + # and is nicer to twork with + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + episode_append_data_list_size=0, + episode_append_data_list=[], + ))) + + if len(self.user_episode_append_id_list) != len(self.user_id_list) != len(self.episode_append_id_list) != len(self.own_num_list): + raise ValueError("all lists must be of the same length") + + for i in range(len(self.user_id_list)): + # add the episode_data_struct to the resp_struct.episode_append_data_list + resp_data.episode_append_data_list.append(dict( + user_episode_append_id_size=len(self.user_episode_append_id_list[i]) * 2, + user_episode_append_id=[ord(x) for x in self.user_episode_append_id_list[i]], + user_id_size=len(self.user_id_list[i]) * 2, + user_id=[ord(x) for x in self.user_id_list[i]], + episode_append_id=self.episode_append_id_list[i], + own_num=self.own_num_list[i], + )) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetPartyDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.party_data_list_size = 1 # Number of arrays + + self.user_party_id = "0" + self.team_no = 0 + self.party_team_data_list_size = 3 # Number of arrays + + self.user_party_team_id_1 = "0" + self.arrangement_num_1 = 0 + self.user_hero_log_id_1 = "101000010" + self.main_weapon_user_equipment_id_1 = "101000016" + self.sub_equipment_user_equipment_id_1 = "0" + self.skill_slot1_skill_id_1 = 30086 + self.skill_slot2_skill_id_1 = 1001 + self.skill_slot3_skill_id_1 = 1002 + self.skill_slot4_skill_id_1 = 1003 + self.skill_slot5_skill_id_1 = 1005 + + self.user_party_team_id_2 = "0" + self.arrangement_num_2 = 0 + self.user_hero_log_id_2 = "102000010" + self.main_weapon_user_equipment_id_2 = "103000006" + self.sub_equipment_user_equipment_id_2 = "0" + self.skill_slot1_skill_id_2 = 30086 + self.skill_slot2_skill_id_2 = 1001 + self.skill_slot3_skill_id_2 = 1002 + self.skill_slot4_skill_id_2 = 1003 + self.skill_slot5_skill_id_2 = 1005 + + self.user_party_team_id_3 = "0" + self.arrangement_num_3 = 0 + self.user_hero_log_id_3 = "103000010" + self.main_weapon_user_equipment_id_3 = "112000009" + self.sub_equipment_user_equipment_id_3 = "0" + self.skill_slot1_skill_id_3 = 30086 + self.skill_slot2_skill_id_3 = 1001 + self.skill_slot3_skill_id_3 = 1002 + self.skill_slot4_skill_id_3 = 1003 + self.skill_slot5_skill_id_3 = 1005 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "party_data_list_size" / Int32ub, # big endian + + "user_party_id_size" / Int32ub, # big endian + "user_party_id" / Int16ul[len(self.user_party_id)], + "team_no" / Int8ul, # result is either 0 or 1 + "party_team_data_list_size" / Int32ub, # big endian + + "user_party_team_id_1_size" / Int32ub, # big endian + "user_party_team_id_1" / Int16ul[len(self.user_party_team_id_1)], + "arrangement_num_1" / Int8ul, # big endian + "user_hero_log_id_1_size" / Int32ub, # big endian + "user_hero_log_id_1" / Int16ul[len(self.user_hero_log_id_1)], + "main_weapon_user_equipment_id_1_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_1" / Int16ul[len(self.main_weapon_user_equipment_id_1)], + "sub_equipment_user_equipment_id_1_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_1" / Int16ul[len(self.sub_equipment_user_equipment_id_1)], + "skill_slot1_skill_id_1" / Int32ub, + "skill_slot2_skill_id_1" / Int32ub, + "skill_slot3_skill_id_1" / Int32ub, + "skill_slot4_skill_id_1" / Int32ub, + "skill_slot5_skill_id_1" / Int32ub, + + "user_party_team_id_2_size" / Int32ub, # big endian + "user_party_team_id_2" / Int16ul[len(self.user_party_team_id_2)], + "arrangement_num_2" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_2_size" / Int32ub, # big endian + "user_hero_log_id_2" / Int16ul[len(self.user_hero_log_id_2)], + "main_weapon_user_equipment_id_2_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_2" / Int16ul[len(self.main_weapon_user_equipment_id_2)], + "sub_equipment_user_equipment_id_2_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_2" / Int16ul[len(self.sub_equipment_user_equipment_id_2)], + "skill_slot1_skill_id_2" / Int32ub, + "skill_slot2_skill_id_2" / Int32ub, + "skill_slot3_skill_id_2" / Int32ub, + "skill_slot4_skill_id_2" / Int32ub, + "skill_slot5_skill_id_2" / Int32ub, + + "user_party_team_id_3_size" / Int32ub, # big endian + "user_party_team_id_3" / Int16ul[len(self.user_party_team_id_3)], + "arrangement_num_3" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_3_size" / Int32ub, # big endian + "user_hero_log_id_3" / Int16ul[len(self.user_hero_log_id_3)], + "main_weapon_user_equipment_id_3_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_3" / Int16ul[len(self.main_weapon_user_equipment_id_3)], + "sub_equipment_user_equipment_id_3_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_3" / Int16ul[len(self.sub_equipment_user_equipment_id_3)], + "skill_slot1_skill_id_3" / Int32ub, + "skill_slot2_skill_id_3" / Int32ub, + "skill_slot3_skill_id_3" / Int32ub, + "skill_slot4_skill_id_3" / Int32ub, + "skill_slot5_skill_id_3" / Int32ub, + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + party_data_list_size=self.party_data_list_size, + + user_party_id_size=len(self.user_party_id) * 2, + user_party_id=[ord(x) for x in self.user_party_id], + team_no=self.team_no, + party_team_data_list_size=self.party_team_data_list_size, + + user_party_team_id_1_size=len(self.user_party_team_id_1) * 2, + user_party_team_id_1=[ord(x) for x in self.user_party_team_id_1], + arrangement_num_1=self.arrangement_num_1, + user_hero_log_id_1_size=len(self.user_hero_log_id_1) * 2, + user_hero_log_id_1=[ord(x) for x in self.user_hero_log_id_1], + main_weapon_user_equipment_id_1_size=len(self.main_weapon_user_equipment_id_1) * 2, + main_weapon_user_equipment_id_1=[ord(x) for x in self.main_weapon_user_equipment_id_1], + sub_equipment_user_equipment_id_1_size=len(self.sub_equipment_user_equipment_id_1) * 2, + sub_equipment_user_equipment_id_1=[ord(x) for x in self.sub_equipment_user_equipment_id_1], + skill_slot1_skill_id_1=self.skill_slot1_skill_id_1, + skill_slot2_skill_id_1=self.skill_slot2_skill_id_1, + skill_slot3_skill_id_1=self.skill_slot3_skill_id_1, + skill_slot4_skill_id_1=self.skill_slot4_skill_id_1, + skill_slot5_skill_id_1=self.skill_slot5_skill_id_1, + + user_party_team_id_2_size=len(self.user_party_team_id_2) * 2, + user_party_team_id_2=[ord(x) for x in self.user_party_team_id_2], + arrangement_num_2=self.arrangement_num_2, + user_hero_log_id_2_size=len(self.user_hero_log_id_2) * 2, + user_hero_log_id_2=[ord(x) for x in self.user_hero_log_id_2], + main_weapon_user_equipment_id_2_size=len(self.main_weapon_user_equipment_id_2) * 2, + main_weapon_user_equipment_id_2=[ord(x) for x in self.main_weapon_user_equipment_id_2], + sub_equipment_user_equipment_id_2_size=len(self.sub_equipment_user_equipment_id_2) * 2, + sub_equipment_user_equipment_id_2=[ord(x) for x in self.sub_equipment_user_equipment_id_2], + skill_slot1_skill_id_2=self.skill_slot1_skill_id_2, + skill_slot2_skill_id_2=self.skill_slot2_skill_id_2, + skill_slot3_skill_id_2=self.skill_slot3_skill_id_2, + skill_slot4_skill_id_2=self.skill_slot4_skill_id_2, + skill_slot5_skill_id_2=self.skill_slot5_skill_id_2, + + user_party_team_id_3_size=len(self.user_party_team_id_3) * 2, + user_party_team_id_3=[ord(x) for x in self.user_party_team_id_3], + arrangement_num_3=self.arrangement_num_3, + user_hero_log_id_3_size=len(self.user_hero_log_id_3) * 2, + user_hero_log_id_3=[ord(x) for x in self.user_hero_log_id_3], + main_weapon_user_equipment_id_3_size=len(self.main_weapon_user_equipment_id_3) * 2, + main_weapon_user_equipment_id_3=[ord(x) for x in self.main_weapon_user_equipment_id_3], + sub_equipment_user_equipment_id_3_size=len(self.sub_equipment_user_equipment_id_3) * 2, + sub_equipment_user_equipment_id_3=[ord(x) for x in self.sub_equipment_user_equipment_id_3], + skill_slot1_skill_id_3=self.skill_slot1_skill_id_3, + skill_slot2_skill_id_3=self.skill_slot2_skill_id_3, + skill_slot3_skill_id_3=self.skill_slot3_skill_id_3, + skill_slot4_skill_id_3=self.skill_slot4_skill_id_3, + skill_slot5_skill_id_3=self.skill_slot5_skill_id_3, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.profile_card_data = 1 # number of arrays + + self.profile_card_code = "" + self.nick_name = "" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "profile_card_data" / Int32ub, # big endian + + "profile_card_code_size" / Int32ub, # big endian + "profile_card_code" / Int16ul[len(self.profile_card_code)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, #short + "setting_title_id" / Int32ub, # int + "skill_id" / Int16ub, # short + "hero_log_hero_log_id" / Int32ub, # int + "hero_log_log_level" / Int16ub, # short + "hero_log_awakening_stage" / Int16ub, # short + "hero_log_property1_property_id" / Int32ub, # int + "hero_log_property1_value1" / Int32ub, # int + "hero_log_property1_value2" / Int32ub, # int + "hero_log_property2_property_id" / Int32ub, # int + "hero_log_property2_value1" / Int32ub, # int + "hero_log_property2_value2" / Int32ub, # int + "hero_log_property3_property_id" / Int32ub, # int + "hero_log_property3_value1" / Int32ub, # int + "hero_log_property3_value2" / Int32ub, # int + "hero_log_property4_property_id" / Int32ub, # int + "hero_log_property4_value1" / Int32ub, # int + "hero_log_property4_value2" / Int32ub, # int + "main_weapon_equipment_id" / Int32ub, # int + "main_weapon_enhancement_value" / Int16ub, # short + "main_weapon_awakening_stage" / Int16ub, # short + "main_weapon_property1_property_id" / Int32ub, # int + "main_weapon_property1_value1" / Int32ub, # int + "main_weapon_property1_value2" / Int32ub, # int + "main_weapon_property2_property_id" / Int32ub, # int + "main_weapon_property2_value1" / Int32ub, # int + "main_weapon_property2_value2" / Int32ub, # int + "main_weapon_property3_property_id" / Int32ub, # int + "main_weapon_property3_value1" / Int32ub, # int + "main_weapon_property3_value2" / Int32ub, # int + "main_weapon_property4_property_id" / Int32ub, # int + "main_weapon_property4_value1" / Int32ub, # int + "main_weapon_property4_value2" / Int32ub, # int + "sub_equipment_equipment_id" / Int32ub, # int + "sub_equipment_enhancement_value" / Int16ub, # short + "sub_equipment_awakening_stage" / Int16ub, # short + "sub_equipment_property1_property_id" / Int32ub, # int + "sub_equipment_property1_value1" / Int32ub, # int + "sub_equipment_property1_value2" / Int32ub, # int + "sub_equipment_property2_property_id" / Int32ub, # int + "sub_equipment_property2_value1" / Int32ub, # int + "sub_equipment_property2_value2" / Int32ub, # int + "sub_equipment_property3_property_id" / Int32ub, # int + "sub_equipment_property3_value1" / Int32ub, # int + "sub_equipment_property3_value2" / Int32ub, # int + "sub_equipment_property4_property_id" / Int32ub, # int + "sub_equipment_property4_value1" / Int32ub, # int + "sub_equipment_property4_value2" / Int32ub, # int + "holographic_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + profile_card_data=self.profile_card_data, + + profile_card_code_size=len(self.profile_card_code) * 2, + profile_card_code=[ord(x) for x in self.profile_card_code], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=0, + setting_title_id=0, + skill_id=0, + hero_log_hero_log_id=0, + hero_log_log_level=0, + hero_log_awakening_stage=0, + hero_log_property1_property_id=0, + hero_log_property1_value1=0, + hero_log_property1_value2=0, + hero_log_property2_property_id=0, + hero_log_property2_value1=0, + hero_log_property2_value2=0, + hero_log_property3_property_id=0, + hero_log_property3_value1=0, + hero_log_property3_value2=0, + hero_log_property4_property_id=0, + hero_log_property4_value1=0, + hero_log_property4_value2=0, + main_weapon_equipment_id=0, + main_weapon_enhancement_value=0, + main_weapon_awakening_stage=0, + main_weapon_property1_property_id=0, + main_weapon_property1_value1=0, + main_weapon_property1_value2=0, + main_weapon_property2_property_id=0, + main_weapon_property2_value1=0, + main_weapon_property2_value2=0, + main_weapon_property3_property_id=0, + main_weapon_property3_value1=0, + main_weapon_property3_value2=0, + main_weapon_property4_property_id=0, + main_weapon_property4_value1=0, + main_weapon_property4_value2=0, + sub_equipment_equipment_id=0, + sub_equipment_enhancement_value=0, + sub_equipment_awakening_stage=0, + sub_equipment_property1_property_id=0, + sub_equipment_property1_value1=0, + sub_equipment_property1_value2=0, + sub_equipment_property2_property_id=0, + sub_equipment_property2_value1=0, + sub_equipment_property2_value2=0, + sub_equipment_property3_property_id=0, + sub_equipment_property3_value1=0, + sub_equipment_property3_value2=0, + sub_equipment_property4_property_id=0, + sub_equipment_property4_value1=0, + sub_equipment_property4_value2=0, + holographic_flag=0, + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetResourcePathInfoRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetResourcePathInfoResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.resource_base_url = "http://localhost:9000/SDEW/100/" + self.gasha_base_dir = "a" + self.ad_base_dir = "b" + self.event_base_dir = "c" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "resource_base_url_size" / Int32ub, # big endian + "resource_base_url" / Int16ul[len(self.resource_base_url)], + "gasha_base_dir_size" / Int32ub, # big endian + "gasha_base_dir" / Int16ul[len(self.gasha_base_dir)], + "ad_base_dir_size" / Int32ub, # big endian + "ad_base_dir" / Int16ul[len(self.ad_base_dir)], + "event_base_dir_size" / Int32ub, # big endian + "event_base_dir" / Int16ul[len(self.event_base_dir)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + resource_base_url_size=len(self.resource_base_url) * 2, + resource_base_url=[ord(x) for x in self.resource_base_url], + gasha_base_dir_size=len(self.gasha_base_dir) * 2, + gasha_base_dir=[ord(x) for x in self.gasha_base_dir], + ad_base_dir_size=len(self.ad_base_dir) * 2, + ad_base_dir=[ord(x) for x in self.ad_base_dir], + event_base_dir_size=len(self.event_base_dir) * 2, + event_base_dir=[ord(x) for x in self.event_base_dir], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayStartRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayStartResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.play_start_response_data_size = 1 # Number of arrays (minimum 1 mandatory) + self.multi_play_start_response_data_size = 0 # Number of arrays (set 0 due to single play) + + self.appearance_player_trace_data_list_size = 1 + + self.user_quest_scene_player_trace_id = "1003" + self.nick_name = profile_data["nick_name"] + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_start_response_data_size" / Int32ub, + "multi_play_start_response_data_size" / Int32ub, + + "appearance_player_trace_data_list_size" / Int32ub, + + "user_quest_scene_player_trace_id_size" / Int32ub, # big endian + "user_quest_scene_player_trace_id" / Int16ul[len(self.user_quest_scene_player_trace_id)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_start_response_data_size=self.play_start_response_data_size, + multi_play_start_response_data_size=self.multi_play_start_response_data_size, + + appearance_player_trace_data_list_size=self.appearance_player_trace_data_list_size, + + user_quest_scene_player_trace_id_size=len(self.user_quest_scene_player_trace_id) * 2, + user_quest_scene_player_trace_id=[ord(x) for x in self.user_quest_scene_player_trace_id], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayEndRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayEndResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_response_data_size = 1 # Number of arrays + self.multi_play_end_response_data_size = 1 # Unused on solo play + + self.dummy_1 = 0 + self.dummy_2 = 0 + self.dummy_3 = 0 + + self.rarity_up_occurrence_flag = 0 + self.adventure_ex_area_occurrences_flag = 0 + self.ex_bonus_data_list_size = 1 # Number of arrays + self.play_end_player_trace_reward_data_list_size = 0 # Number of arrays + + self.ex_bonus_table_id = 0 # ExBonusTable.csv values, dont care for now + self.achievement_status = 1 + + self.common_reward_data_size = 1 # Number of arrays + + self.common_reward_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv + self.common_reward_id = 0 + self.common_reward_num = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_end_response_data_size" / Int32ub, # big endian + + "rarity_up_occurrence_flag" / Int8ul, # result is either 0 or 1 + "adventure_ex_area_occurrences_flag" / Int8ul, # result is either 0 or 1 + "ex_bonus_data_list_size" / Int32ub, # big endian + "play_end_player_trace_reward_data_list_size" / Int32ub, # big endian + + # ex_bonus_data_list + "ex_bonus_table_id" / Int32ub, + "achievement_status" / Int8ul, # result is either 0 or 1 + + # play_end_player_trace_reward_data_list + "common_reward_data_size" / Int32ub, + + # common_reward_data + "common_reward_type" / Int16ub, # short + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + + "multi_play_end_response_data_size" / Int32ub, # big endian + + # multi_play_end_response_data + "dummy_1" / Int8ul, # result is either 0 or 1 + "dummy_2" / Int8ul, # result is either 0 or 1 + "dummy_3" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_end_response_data_size=self.play_end_response_data_size, + + rarity_up_occurrence_flag=self.rarity_up_occurrence_flag, + adventure_ex_area_occurrences_flag=self.adventure_ex_area_occurrences_flag, + ex_bonus_data_list_size=self.ex_bonus_data_list_size, + play_end_player_trace_reward_data_list_size=self.play_end_player_trace_reward_data_list_size, + + ex_bonus_table_id=self.ex_bonus_table_id, + achievement_status=self.achievement_status, + + common_reward_data_size=self.common_reward_data_size, + + common_reward_type=self.common_reward_type, + common_reward_id=self.common_reward_id, + common_reward_num=self.common_reward_num, + + multi_play_end_response_data_size=self.multi_play_end_response_data_size, + + dummy_1=self.dummy_1, + dummy_2=self.dummy_2, + dummy_3=self.dummy_3, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_unanalyzed_log_reward_data_list_size = 1 # Number of arrays + + self.unanalyzed_log_grade_id = 3 # RewardTable.csv + self.common_reward_data_size = 1 + + self.common_reward_type_1 = 1 + self.common_reward_id_1 = 102000070 + self.common_reward_num_1 = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_end_unanalyzed_log_reward_data_list_size" / Int32ub, # big endian + + "unanalyzed_log_grade_id" / Int32ub, + "common_reward_data_size" / Int32ub, + + "common_reward_type_1" / Int16ub, + "common_reward_id_1" / Int32ub, + "common_reward_num_1" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_end_unanalyzed_log_reward_data_list_size=self.play_end_unanalyzed_log_reward_data_list_size, + + unanalyzed_log_grade_id=self.unanalyzed_log_grade_id, + common_reward_data_size=self.common_reward_data_size, + + common_reward_type_1=self.common_reward_type_1, + common_reward_id_1=self.common_reward_id_1, + common_reward_num_1=self.common_reward_num_1, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, questIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # quest_scene_user_data_list_size + self.quest_type = [1] * len(questIdsData) + self.quest_scene_id = questIdsData + self.clear_flag = [1] * len(questIdsData) + + # quest_scene_best_score_user_data + self.clear_time = 300 + self.combo_num = 0 + self.total_damage = "0" + self.concurrent_destroying_num = 1 + + # quest_scene_ex_bonus_user_data_list + self.achievement_flag = [[1, 1, 1],[1, 1, 1]] + self.ex_bonus_table_id = [[1, 2, 3],[4, 5, 6]] + + def make(self) -> bytes: + #new stuff + quest_scene_ex_bonus_user_data_list_struct = Struct( + "achievement_flag" / Int32ub, # big endian + "ex_bonus_table_id" / Int32ub, # big endian + ) + + quest_scene_best_score_user_data_struct = Struct( + "clear_time" / Int32ub, # big endian + "combo_num" / Int32ub, # big endian + "total_damage_size" / Int32ub, # big endian + "total_damage" / Int16ul[len(self.total_damage)], + "concurrent_destroying_num" / Int16ub, + ) + + quest_scene_user_data_list_struct = Struct( + "quest_type" / Int8ul, # result is either 0 or 1 + "quest_scene_id" / Int16ub, #short + "clear_flag" / Int8ul, # result is either 0 or 1 + "quest_scene_best_score_user_data_size" / Rebuild(Int32ub, len_(this.quest_scene_best_score_user_data)), # big endian + "quest_scene_best_score_user_data" / Array(this.quest_scene_best_score_user_data_size, quest_scene_best_score_user_data_struct), + "quest_scene_ex_bonus_user_data_list_size" / Rebuild(Int32ub, len_(this.quest_scene_ex_bonus_user_data_list)), # big endian + "quest_scene_ex_bonus_user_data_list" / Array(this.quest_scene_ex_bonus_user_data_list_size, quest_scene_ex_bonus_user_data_list_struct), + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "quest_scene_user_data_list_size" / Rebuild(Int32ub, len_(this.quest_scene_user_data_list)), # big endian + "quest_scene_user_data_list" / Array(this.quest_scene_user_data_list_size, quest_scene_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + quest_scene_user_data_list_size=0, + quest_scene_user_data_list=[], + ))) + + for i in range(len(self.quest_scene_id)): + quest_data = dict( + quest_type=self.quest_type[i], + quest_scene_id=self.quest_scene_id[i], + clear_flag=self.clear_flag[i], + + quest_scene_best_score_user_data_size=0, + quest_scene_best_score_user_data=[], + quest_scene_ex_bonus_user_data_list_size=0, + quest_scene_ex_bonus_user_data_list=[], + ) + + quest_data["quest_scene_best_score_user_data"].append(dict( + clear_time=self.clear_time, + combo_num=self.combo_num, + total_damage_size=len(self.total_damage) * 2, + total_damage=[ord(x) for x in self.total_damage], + concurrent_destroying_num=self.concurrent_destroying_num, + )) + + ''' + quest_data["quest_scene_ex_bonus_user_data_list"].append(dict( + ex_bonus_table_id=self.ex_bonus_table_id[i], + achievement_flag=self.achievement_flag[i], + )) + ''' + + resp_data.quest_scene_user_data_list.append(quest_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.elapsed_days = 1 + self.get_yui_medal_num = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag" / Int8ul, # result is either 0 or 1 + "elapsed_days" / Int16ub, #short + "get_yui_medal_num" / Int16ub, #short + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + elapsed_days=self.elapsed_days, + get_yui_medal_num=self.get_yui_medal_num, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_size = 1 # number of arrays + + self.elapsed_days = 1 + self.loop_num = 1 + self.last_check_date = "20230520193000" + self.last_get_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_size" / Int32ub, # big endian + + "elapsed_days" / Int32ub, # big endian + "loop_num" / Int32ub, # big endian + "last_check_date_size" / Int32ub, # big endian + "last_check_date" / Int16ul[len(self.last_check_date)], + "last_get_date_size" / Int32ub, # big endian + "last_get_date" / Int16ul[len(self.last_get_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_size=self.data_size, + + elapsed_days=self.elapsed_days, + loop_num=self.loop_num, + last_check_date_size=len(self.last_check_date) * 2, + last_check_date=[ord(x) for x in self.last_check_date], + last_get_date_size=len(self.last_get_date) * 2, + last_get_date=[ord(x) for x in self.last_get_date], + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.used_num = 0 + self.get_vp = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag" / Int8ul, # result is either 0 or 1 + "used_num" / Int32ub, # big endian + "get_vp" / Int32ub, # big endian + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + used_num=self.used_num, + get_vp=self.get_vp, + + )) + + self.length = len(resp_data) + return super().make() + resp_data \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py new file mode 100644 index 0000000..2c903ce --- /dev/null +++ b/titles/sao/index.py @@ -0,0 +1,117 @@ +from typing import Tuple +from twisted.web.http import Request +from twisted.web import resource +import json, ast +from datetime import datetime +import yaml +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +import inflection +from os import path + +from core import CoreConfig, Utils +from titles.sao.config import SaoConfig +from titles.sao.const import SaoConstants +from titles.sao.base import SaoBase +from titles.sao.handlers.base import * + + +class SaoServlet(resource.Resource): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.isLeaf = True + self.core_cfg = core_cfg + self.config_dir = cfg_dir + self.game_cfg = SaoConfig() + if path.exists(f"{cfg_dir}/sao.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/sao.yaml"))) + + self.logger = logging.getLogger("sao") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] SAO | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "sao"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + self.base = SaoBase(core_cfg, self.game_cfg) + + @classmethod + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = SaoConfig() + + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "", "") + + return ( + True, + f"http://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", + f"{game_cfg.server.hostname}/SDEW/$v/", + ) + + @classmethod + def get_mucha_info( + cls, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = SaoConfig() + + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "") + + return (True, "SAO1") + + def setup(self) -> None: + pass + + def render_POST( + self, request: Request, version: int = 0, endpoints: str = "" + ) -> bytes: + req_url = request.uri.decode() + if req_url == "/matching": + self.logger.info("Matching request") + + request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + + sao_request = request.content.getvalue().hex() + #sao_request = sao_request[:32] + + handler = getattr(self.base, f"handle_{sao_request[:4]}", None) + if handler is None: + self.logger.info(f"Generic Handler for {req_url} - {sao_request[:4]}") + #self.logger.debug(f"Request: {request.content.getvalue().hex()}") + resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1) + self.logger.debug(f"Response: {resp.make().hex()}") + return resp.make() + + self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") + self.logger.debug(f"Request: {request.content.getvalue().hex()}") + self.logger.debug(f"Response: {handler(sao_request).hex()}") + return handler(sao_request) \ No newline at end of file diff --git a/titles/sao/read.py b/titles/sao/read.py new file mode 100644 index 0000000..5fc9804 --- /dev/null +++ b/titles/sao/read.py @@ -0,0 +1,230 @@ +from typing import Optional, Dict, List +from os import walk, path +import urllib +import csv + +from read import BaseReader +from core.config import CoreConfig +from titles.sao.database import SaoData +from titles.sao.const import SaoConstants + + +class SaoReader(BaseReader): + def __init__( + self, + config: CoreConfig, + version: int, + bin_arg: Optional[str], + opt_arg: Optional[str], + extra: Optional[str], + ) -> None: + super().__init__(config, version, bin_arg, opt_arg, extra) + self.data = SaoData(config) + + try: + self.logger.info( + f"Start importer for {SaoConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid project SAO version {version}") + exit(1) + + def read(self) -> None: + pull_bin_ram = True + + if not path.exists(f"{self.bin_dir}"): + self.logger.warn(f"Couldn't find csv file in {self.bin_dir}, skipping") + pull_bin_ram = False + + if pull_bin_ram: + self.read_csv(f"{self.bin_dir}") + + def read_csv(self, bin_dir: str) -> None: + self.logger.info(f"Read csv from {bin_dir}") + + self.logger.info("Now reading QuestScene.csv") + try: + fullPath = bin_dir + "/QuestScene.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + questSceneId = row["QuestSceneId"] + sortNo = row["SortNo"] + name = row["Name"] + enabled = True + + self.logger.info(f"Added quest {questSceneId} | Name: {name}") + + try: + self.data.static.put_quest( + questSceneId, + 0, + sortNo, + name, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading HeroLog.csv") + try: + fullPath = bin_dir + "/HeroLog.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + heroLogId = row["HeroLogId"] + name = row["Name"] + nickname = row["Nickname"] + rarity = row["Rarity"] + skillTableSubId = row["SkillTableSubId"] + awakeningExp = row["AwakeningExp"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added hero {heroLogId} | Name: {name}") + + try: + self.data.static.put_hero( + 0, + heroLogId, + name, + nickname, + rarity, + skillTableSubId, + awakeningExp, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Equipment.csv") + try: + fullPath = bin_dir + "/Equipment.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + equipmentId = row["EquipmentId"] + equipmentType = row["EquipmentType"] + weaponTypeId = row["WeaponTypeId"] + name = row["Name"] + rarity = row["Rarity"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added equipment {equipmentId} | Name: {name}") + + try: + self.data.static.put_equipment( + 0, + equipmentId, + name, + equipmentType, + weaponTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Item.csv") + try: + fullPath = bin_dir + "/Item.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + itemId = row["ItemId"] + itemTypeId = row["ItemTypeId"] + name = row["Name"] + rarity = row["Rarity"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added item {itemId} | Name: {name}") + + try: + self.data.static.put_item( + 0, + itemId, + name, + itemTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading SupportLog.csv") + try: + fullPath = bin_dir + "/SupportLog.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + supportLogId = row["SupportLogId"] + charaId = row["CharaId"] + name = row["Name"] + rarity = row["Rarity"] + salePrice = row["SalePrice"] + skillName = row["SkillName"] + enabled = True + + self.logger.info(f"Added support log {supportLogId} | Name: {name}") + + try: + self.data.static.put_support_log( + 0, + supportLogId, + charaId, + name, + rarity, + salePrice, + skillName, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Title.csv") + try: + fullPath = bin_dir + "/Title.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + titleId = row["TitleId"] + displayName = row["DisplayName"] + requirement = row["Requirement"] + rank = row["Rank"] + imageFilePath = row["ImageFilePath"] + enabled = True + + self.logger.info(f"Added title {titleId} | Name: {displayName}") + + if len(titleId) > 5: + try: + self.data.static.put_title( + 0, + titleId, + displayName, + requirement, + rank, + imageFilePath, + enabled + ) + except Exception as err: + print(err) + elif len(titleId) < 6: # current server code cannot have multiple lengths for the id + continue + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py new file mode 100644 index 0000000..b4fede2 --- /dev/null +++ b/titles/sao/schema/__init__.py @@ -0,0 +1,2 @@ +from .profile import SaoProfileData +from .static import SaoStaticData \ No newline at end of file diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py new file mode 100644 index 0000000..6ae60b1 --- /dev/null +++ b/titles/sao/schema/profile.py @@ -0,0 +1,48 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from ..const import SaoConstants + +profile = Table( + "sao_profile", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("user_type", Integer, server_default="1"), + Column("nick_name", String(16), server_default="PLAYER"), + Column("rank_num", Integer, server_default="1"), + Column("rank_exp", Integer, server_default="0"), + Column("own_col", Integer, server_default="0"), + Column("own_vp", Integer, server_default="0"), + Column("own_yui_medal", Integer, server_default="0"), + Column("setting_title_id", Integer, server_default="20005"), +) + +class SaoProfileData(BaseData): + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user=user_id) + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO profile for user {user_id}!") + return None + return result.lastrowid + + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py new file mode 100644 index 0000000..9e96aaf --- /dev/null +++ b/titles/sao/schema/static.py @@ -0,0 +1,297 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float +from sqlalchemy.engine.base import Connection +from sqlalchemy.engine import Row +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +quest = Table( + "sao_static_quest", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("questSceneId", Integer), + Column("sortNo", Integer), + Column("name", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "questSceneId", name="sao_static_quest_uk" + ), + mysql_charset="utf8mb4", +) + +hero = Table( + "sao_static_hero_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("heroLogId", Integer), + Column("name", String(255)), + Column("nickname", String(255)), + Column("rarity", Integer), + Column("skillTableSubId", Integer), + Column("awakeningExp", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), + mysql_charset="utf8mb4", +) + +equipment = Table( + "sao_static_equipment_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("equipmentId", Integer), + Column("equipmentType", Integer), + Column("weaponTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), + mysql_charset="utf8mb4", +) + +item = Table( + "sao_static_item_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("itemId", Integer), + Column("itemTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), + mysql_charset="utf8mb4", +) + +support = Table( + "sao_static_support_log_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("supportLogId", Integer), + Column("charaId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("salePrice", Integer), + Column("skillName", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "supportLogId", name="sao_static_support_log_list_uk" + ), + mysql_charset="utf8mb4", +) + +title = Table( + "sao_static_title_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("titleId", Integer), + Column("displayName", String(255)), + Column("requirement", Integer), + Column("rank", Integer), + Column("imageFilePath", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "titleId", name="sao_static_title_list_uk" + ), + mysql_charset="utf8mb4", +) + +class SaoStaticData(BaseData): + def put_quest( self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool ) -> Optional[int]: + sql = insert(quest).values( + questSceneId=questSceneId, + version=version, + sortNo=sortNo, + name=name, + tutorial=tutorial, + ) + + conflict = sql.on_duplicate_key_update( + name=name, questSceneId=questSceneId, version=version + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_hero( self, version: int, heroLogId: int, name: str, nickname: str, rarity: int, skillTableSubId: int, awakeningExp: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(hero).values( + version=version, + heroLogId=heroLogId, + name=name, + nickname=nickname, + rarity=rarity, + skillTableSubId=skillTableSubId, + awakeningExp=awakeningExp, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, heroLogId=heroLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_equipment( self, version: int, equipmentId: int, name: str, equipmentType: int, weaponTypeId:int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(equipment).values( + version=version, + equipmentId=equipmentId, + name=name, + equipmentType=equipmentType, + weaponTypeId=weaponTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, equipmentId=equipmentId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_item( self, version: int, itemId: int, name: str, itemTypeId: int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(item).values( + version=version, + itemId=itemId, + name=name, + itemTypeId=itemTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, itemId=itemId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_support_log( self, version: int, supportLogId: int, charaId: int, name: str, rarity: int, salePrice: int, skillName: str, enabled: bool ) -> Optional[int]: + sql = insert(support).values( + version=version, + supportLogId=supportLogId, + charaId=charaId, + name=name, + rarity=rarity, + salePrice=salePrice, + skillName=skillName, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, supportLogId=supportLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]: + sql = insert(title).values( + version=version, + titleId=titleId, + displayName=displayName, + requirement=requirement, + rank=rank, + imageFilePath=imageFilePath, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + displayName=displayName, titleId=titleId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by( + quest.c.questSceneId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = hero.select(hero.c.version == version and hero.c.enabled == enabled).order_by( + hero.c.heroLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = equipment.select(equipment.c.version == version and equipment.c.enabled == enabled).order_by( + equipment.c.equipmentId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by( + item.c.itemId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_support_log_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = support.select(support.c.version == version and support.c.enabled == enabled).order_by( + support.c.supportLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_title_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = title.select(title.c.version == version and title.c.enabled == enabled).order_by( + title.c.titleId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] \ No newline at end of file From cab1d6814a055161f8572794e4f95036f18857ba Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 26 May 2023 13:57:16 -0400 Subject: [PATCH 228/589] added game specifics for SAO --- docs/game_specific_info.md | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 5f04f93..a02e7bc 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -15,6 +15,7 @@ using the megaime database. Clean installations always create the latest databas - [O.N.G.E.K.I.](#o-n-g-e-k-i) - [Card Maker](#card-maker) - [WACCA](#wacca) + - [Sword Art Online Arcade](#sao) # Supported Games @@ -365,3 +366,47 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core ```shell python dbutils.py --game SDFE upgrade ``` + +## SAO + +### SDWS + +| Version ID | Version Name | +|------------|---------------| +| 0 | SAO | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDEW --version --binfolder /path/to/game/extractedassets +``` + +The importer for SAO will import all items, heroes, support skills and titles data. + +### Config + +Config file is located in `config/sao.yaml`. + +| Option | Info | +|--------------------|-----------------------------------------------------------------------------| +| `hostname` | Changes the server listening address for Mucha | +| `port` | Changes the listing port | +| `auto_register` | Allows the game to handle the automatic registration of new cards | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDEW upgrade +``` + +### Credits for SAO support: + +Midorica - Limited Network Support +Dniel97 - Helping with network base +tungnotpunk - Source \ No newline at end of file From 049dc40a8b672ce23865135710ec3232e1679c5d Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 26 May 2023 14:04:49 -0400 Subject: [PATCH 229/589] small typo of the documentation --- 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 a02e7bc..af2afbe 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -369,7 +369,7 @@ python dbutils.py --game SDFE upgrade ## SAO -### SDWS +### SDEW | Version ID | Version Name | |------------|---------------| From 05dee87a9a3a537dc2869edcc2aeec0c96966bd3 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 26 May 2023 21:41:16 -0400 Subject: [PATCH 230/589] allnet: update default values, add debug log for unknown but allowed auths --- core/allnet.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index d54b333..32ae177 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -112,6 +112,8 @@ class AllnetServlet: ) resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + + self.logger.debug(f"Allnet response: {vars(resp)}") return self.dict_to_http_form_string([vars(resp)]) resp.uri, resp.host = self.uri_registry[req.game_id] @@ -410,8 +412,8 @@ class AllnetPowerOnResponse3: self.uri = "" self.host = "" self.place_id = "123" - self.name = "" - self.nickname = "" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" self.region0 = "1" self.region_name0 = "W" self.region_name1 = "" @@ -434,8 +436,8 @@ class AllnetPowerOnResponse2: self.uri = "" self.host = "" self.place_id = "123" - self.name = "Test" - self.nickname = "Test123" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" self.region0 = "1" self.region_name0 = "W" self.region_name1 = "X" From 84cb786bdef048362c82896642ba23d86805235e Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 29 May 2023 11:10:32 -0400 Subject: [PATCH 231/589] Adding some structs for SAO for later use --- docs/game_specific_info.md | 6 +- titles/sao/base.py | 173 ++++++++++++++++++++++++++++++++++++- 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index af2afbe..5361ce9 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -407,6 +407,6 @@ python dbutils.py --game SDEW upgrade ### Credits for SAO support: -Midorica - Limited Network Support -Dniel97 - Helping with network base -tungnotpunk - Source \ No newline at end of file +- Midorica - Limited Network Support +- Dniel97 - Helping with network base +- tungnotpunk - Source \ No newline at end of file diff --git a/titles/sao/base.py b/titles/sao/base.py index 67a2d6b..1e1b02b 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -197,17 +197,186 @@ class SaoBase: #home/check_profile_card_used_reward resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() + + def handle_c806(self, request: Any) -> bytes: + #custom/change_party + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "act_type" / Int8ub, # play_mode is a byte + Padding(3), + "party_data_list_length" / Rebuild(Int8ub, len_(this.party_data_list)), # party_data_list is a byte, + "party_data_list" / Array(this.party_data_list_length, Struct( + "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id + "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string + "team_no" / Int8ub, # team_no is a byte + Padding(3), + "party_team_data_list_length" / Rebuild(Int8ub, len_(this.party_team_data_list)), # party_team_data_list is a byte + "party_team_data_list" / Array(this.party_team_data_list_length, Struct( + "user_party_team_id_size" / Rebuild(Int32ub, len_(this.user_party_team_id) * 2), # calculates the length of the user_party_team_id + "user_party_team_id" / PaddedString(this.user_party_team_id_size, "utf_16_le"), # user_party_team_id is a (zero) padded string + "arrangement_num" / Int8ub, # arrangement_num is a byte + "user_hero_log_id_size" / Rebuild(Int32ub, len_(this.user_hero_log_id) * 2), # calculates the length of the user_hero_log_id + "user_hero_log_id" / PaddedString(this.user_hero_log_id_size, "utf_16_le"), # user_hero_log_id is a (zero) padded string + "main_weapon_user_equipment_id_size" / Rebuild(Int32ub, len_(this.main_weapon_user_equipment_id) * 2), # calculates the length of the main_weapon_user_equipment_id + "main_weapon_user_equipment_id" / PaddedString(this.main_weapon_user_equipment_id_size, "utf_16_le"), # main_weapon_user_equipment_id is a (zero) padded string + "sub_equipment_user_equipment_id_size" / Rebuild(Int32ub, len_(this.sub_equipment_user_equipment_id) * 2), # calculates the length of the sub_equipment_user_equipment_id + "sub_equipment_user_equipment_id" / PaddedString(this.sub_equipment_user_equipment_id_size, "utf_16_le"), # sub_equipment_user_equipment_id is a (zero) padded string + "skill_slot1_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot2_skill_id" / Int32ub, # skill_slot2_skill_id is a int, + "skill_slot3_skill_id" / Int32ub, # skill_slot3_skill_id is a int, + "skill_slot4_skill_id" / Int32ub, # skill_slot4_skill_id is a int, + "skill_slot5_skill_id" / Int32ub, # skill_slot5_skill_id is a int, + )), + )), + + ) + + req_data = req_struct.parse(req) + + #self.logger.info(f"User Team Data: { req_data }") + + resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() def handle_c904(self, request: Any) -> bytes: #quest/episode_play_start - user_id = bytes.fromhex(request[100:124]).decode("utf-16le") + + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "episode_id" / Int32ub, # episode_id is a int, + "play_mode" / Int8ub, # play_mode is a byte + Padding(3), + "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, + "play_start_request_data" / Array(this.play_start_request_data_length, Struct( + "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id + "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string + "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage + "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string + "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage + "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string + "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte + )), + + ) + + req_data = req_struct.parse(req) + + user_id = req_data.user_id profile_data = self.game_data.profile.get_profile(user_id) resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) return resp.make() - def handle_c908(self, request: Any) -> bytes: # function not working yet, tired of this + def handle_c908(self, request: Any) -> bytes: #quest/episode_play_end + + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + Padding(2), + "episode_id" / Int16ub, # episode_id is a short, + Padding(3), + "play_end_request_data" / Int8ub, # play_end_request_data is a byte + Padding(1), + "play_result_flag" / Int8ub, # play_result_flag is a byte + Padding(2), + "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, + "base_get_data" / Array(this.base_get_data_length, Struct( + "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int + "get_col" / Int32ub, # get_num is a short + )), + Padding(3), + "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte + "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( + "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int + )), + Padding(3), + "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte + "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( + "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int + )), + Padding(3), + "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte + "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( + "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int + )), + Padding(3), + "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte + "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( + "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, + )), + Padding(3), + "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, + "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( + "event_item_id" / Int32ub, # event_item_id is an int + "get_num" / Int16ub, # get_num is a short + )), + Padding(3), + "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte + "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte + "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( + "boss_type" / Int8ub, # boss_type is a byte + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte + "mission_data_list" / Array(this.mission_data_list_length, Struct( + "mission_id" / Int32ub, # enemy_kind_id is an int + "clear_flag" / Int8ub, # boss_type is a byte + "mission_difficulty_id" / Int16ub, # destroy_num is a short + )), + Padding(3), + "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte + "score_data" / Array(this.score_data_length, Struct( + "clear_time" / Int32ub, # clear_time is an int + "combo_num" / Int32ub, # boss_type is a int + "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage + "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string + "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short + "reaching_skill_level" / Int16ub, # reaching_skill_level is a short + "ko_chara_num" / Int8ub, # ko_chara_num is a byte + "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short + "boss_destroying_num" / Int16ub, # boss_destroying_num is a short + "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte + "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int + "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte + "continue_cnt" / Int16ub, # continue_cnt is a short + "total_loss_num" / Int16ub, # total_loss_num is a short + )), + + ) + + req_data = req_struct.parse(req) + + #self.logger.info(f"User Get Col Data: { req_data.get_col }") + #self.logger.info(f"User Hero Log Exp Data: { req_data.get_hero_log_exp }") + #self.logger.info(f"User Score Data: { req_data.score_data[0] }") + #self.logger.info(f"User Discovery Enemy Data: { req_data.discovery_enemy_data_list }") + #self.logger.info(f"User Mission Data: { req_data.mission_data_list }") + resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() From d8af7be4a49b8d0d3fd5c456c0ed5925b8c333b7 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 29 May 2023 16:51:41 -0400 Subject: [PATCH 232/589] Adding SAO item table and adding party saving --- titles/sao/base.py | 77 ++++++++++++---- titles/sao/handlers/base.py | 128 +++++++++++++++------------ titles/sao/schema/__init__.py | 3 +- titles/sao/schema/item.py | 161 ++++++++++++++++++++++++++++++++++ 4 files changed, 299 insertions(+), 70 deletions(-) create mode 100644 titles/sao/schema/item.py diff --git a/titles/sao/base.py b/titles/sao/base.py index 1e1b02b..685d330 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -73,7 +73,12 @@ class SaoBase: user_id = -1 self.logger.error("Failed to register card!") + # Create profile with 3 basic heroes profile_id = self.game_data.profile.create_profile(user_id) + self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) self.logger.info(f"User Authenticated: { access_code } | { user_id }") @@ -121,9 +126,19 @@ class SaoBase: def handle_c600(self, request: Any) -> bytes: #have_object/get_hero_log_user_data_list - heroIdsData = self.game_data.static.get_hero_ids(0, True) + req = bytes.fromhex(request)[24:] + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + hero_data = self.game_data.item.get_hero_logs(user_id) - resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, heroIdsData) + resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero_data) return resp.make() def handle_c602(self, request: Any) -> bytes: @@ -164,7 +179,23 @@ class SaoBase: def handle_c804(self, request: Any) -> bytes: #custom/get_party_data_list - resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + + req = bytes.fromhex(request)[24:] + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + hero_party = self.game_data.item.get_hero_party(user_id, 0) + hero1_data = self.game_data.item.get_hero_log(user_id, hero_party[3]) + hero2_data = self.game_data.item.get_hero_log(user_id, hero_party[4]) + hero3_data = self.game_data.item.get_hero_log(user_id, hero_party[5]) + + resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero1_data, hero2_data, hero3_data) return resp.make() def handle_c902(self, request: Any) -> bytes: # for whatever reason, having all entries empty or filled changes nothing @@ -197,7 +228,7 @@ class SaoBase: #home/check_profile_card_used_reward resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - + def handle_c806(self, request: Any) -> bytes: #custom/change_party req = bytes.fromhex(request)[24:] @@ -228,18 +259,40 @@ class SaoBase: "sub_equipment_user_equipment_id_size" / Rebuild(Int32ub, len_(this.sub_equipment_user_equipment_id) * 2), # calculates the length of the sub_equipment_user_equipment_id "sub_equipment_user_equipment_id" / PaddedString(this.sub_equipment_user_equipment_id_size, "utf_16_le"), # sub_equipment_user_equipment_id is a (zero) padded string "skill_slot1_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot2_skill_id" / Int32ub, # skill_slot2_skill_id is a int, - "skill_slot3_skill_id" / Int32ub, # skill_slot3_skill_id is a int, - "skill_slot4_skill_id" / Int32ub, # skill_slot4_skill_id is a int, - "skill_slot5_skill_id" / Int32ub, # skill_slot5_skill_id is a int, + "skill_slot2_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot3_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot4_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot5_skill_id" / Int32ub, # skill_slot1_skill_id is a int, )), )), ) req_data = req_struct.parse(req) + user_id = req_data.user_id - #self.logger.info(f"User Team Data: { req_data }") + for party_team in req_data.party_data_list[0].party_team_data_list: + hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) + hero_level = 1 + hero_exp = 0 + + if hero_data: + hero_level = hero_data["log_level"] + hero_exp = hero_data["log_exp"] + + self.game_data.item.put_hero_log( + user_id, + party_team["user_hero_log_id"], + hero_level, + hero_exp, + party_team["main_weapon_user_equipment_id"], + party_team["sub_equipment_user_equipment_id"], + party_team["skill_slot1_skill_id"], + party_team["skill_slot2_skill_id"], + party_team["skill_slot3_skill_id"], + party_team["skill_slot4_skill_id"], + party_team["skill_slot5_skill_id"] + ) resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -371,12 +424,6 @@ class SaoBase: req_data = req_struct.parse(req) - #self.logger.info(f"User Get Col Data: { req_data.get_col }") - #self.logger.info(f"User Hero Log Exp Data: { req_data.get_hero_log_exp }") - #self.logger.info(f"User Score Data: { req_data.score_data[0] }") - #self.logger.info(f"User Discovery Enemy Data: { req_data.discovery_enemy_data_list }") - #self.logger.info(f"User Mission Data: { req_data.mission_data_list }") - resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 9048517..3bd6d9c 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -478,28 +478,49 @@ class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, heroIdsData) -> None: + def __init__(self, cmd, hero_data) -> None: super().__init__(cmd) self.result = 1 - #print(heroIdsData) - #print(list(map(str,heroIdsData))) + self.user_hero_log_id = [] + self.log_level = [] + self.max_log_level_extended_num = [] + self.log_exp = [] + self.last_set_skill_slot1_skill_id = [] + self.last_set_skill_slot2_skill_id = [] + self.last_set_skill_slot3_skill_id = [] + self.last_set_skill_slot4_skill_id = [] + self.last_set_skill_slot5_skill_id = [] + + for i in range(len(hero_data)): + self.user_hero_log_id.append(hero_data[i][2]) + self.log_level.append(hero_data[i][3]) + self.max_log_level_extended_num.append(hero_data[i][3]) + self.log_exp.append(hero_data[i][4]) + self.last_set_skill_slot1_skill_id.append(hero_data[i][7]) + self.last_set_skill_slot2_skill_id.append(hero_data[i][8]) + self.last_set_skill_slot3_skill_id.append(hero_data[i][9]) + self.last_set_skill_slot4_skill_id.append(hero_data[i][10]) + self.last_set_skill_slot5_skill_id.append(hero_data[i][11]) + + #print(self.user_hero_log_id) + #print(list(map(str,self.user_hero_log_id))) # hero_log_user_data_list - self.user_hero_log_id = list(map(str,heroIdsData)) #str - self.hero_log_id = heroIdsData #int - self.log_level = 10 #short - self.max_log_level_extended_num = 10 #short - self.log_exp = 1000 #int + self.user_hero_log_id = list(map(str,self.user_hero_log_id)) #str + self.hero_log_id = list(map(int,self.user_hero_log_id)) #int + self.log_level = list(map(int,self.log_level)) #short + self.max_log_level_extended_num = list(map(int,self.log_level)) #short + self.log_exp = list(map(int,self.log_level)) #int self.possible_awakening_flag = 0 #byte self.awakening_stage = 0 #short self.awakening_exp = 0 #int self.skill_slot_correction_value = 0 #byte - self.last_set_skill_slot1_skill_id = 0 #short - self.last_set_skill_slot2_skill_id = 0 #short - self.last_set_skill_slot3_skill_id = 0 #short - self.last_set_skill_slot4_skill_id = 0 #short - self.last_set_skill_slot5_skill_id = 0 #short + self.last_set_skill_slot1_skill_id = list(map(int,self.last_set_skill_slot1_skill_id)) #short + self.last_set_skill_slot2_skill_id = list(map(int,self.last_set_skill_slot2_skill_id)) #short + self.last_set_skill_slot3_skill_id = list(map(int,self.last_set_skill_slot3_skill_id)) #short + self.last_set_skill_slot4_skill_id = list(map(int,self.last_set_skill_slot4_skill_id)) #short + self.last_set_skill_slot5_skill_id = list(map(int,self.last_set_skill_slot5_skill_id)) #short self.property1_property_id = 0 #int self.property1_value1 = 0 #int self.property1_value2 = 0 #int @@ -573,18 +594,18 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): user_hero_log_id_size=len(self.user_hero_log_id[i]) * 2, user_hero_log_id=[ord(x) for x in self.user_hero_log_id[i]], hero_log_id=self.hero_log_id[i], - log_level=self.log_level, - max_log_level_extended_num=self.max_log_level_extended_num, - log_exp=self.log_exp, + log_level=self.log_level[i], + max_log_level_extended_num=self.max_log_level_extended_num[i], + log_exp=self.log_exp[i], possible_awakening_flag=self.possible_awakening_flag, awakening_stage=self.awakening_stage, awakening_exp=self.awakening_exp, skill_slot_correction_value=self.skill_slot_correction_value, - last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, - last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, - last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, - last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, - last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id[i], + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id[i], + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id[i], + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id[i], + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id[i], property1_property_id=self.property1_property_id, property1_value1=self.property1_value1, property1_value2=self.property1_value2, @@ -926,10 +947,10 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): def make(self) -> bytes: episode_data_struct = Struct( - "user_episode_append_id_size" / Int32ub, # big endian - "user_episode_append_id" / Int16ul[5], #forced to match the user_episode_append_id_list index which is always 5 chars for the episode ids - "user_id_size" / Int32ub, # big endian - "user_id" / Int16ul[6], # has to be exactly 6 chars in the user field... MANDATORY + "user_episode_append_id_size" / Rebuild(Int32ub, len_(this.user_episode_append_id) * 2), # calculates the length of the user_episode_append_id + "user_episode_append_id" / PaddedString(this.user_episode_append_id_size, "utf_16_le"), # user_episode_append_id is a (zero) padded string + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string "episode_append_id" / Int32ub, "own_num" / Int32ub, ) @@ -955,10 +976,8 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): for i in range(len(self.user_id_list)): # add the episode_data_struct to the resp_struct.episode_append_data_list resp_data.episode_append_data_list.append(dict( - user_episode_append_id_size=len(self.user_episode_append_id_list[i]) * 2, - user_episode_append_id=[ord(x) for x in self.user_episode_append_id_list[i]], - user_id_size=len(self.user_id_list[i]) * 2, - user_id=[ord(x) for x in self.user_id_list[i]], + user_episode_append_id=self.user_episode_append_id_list[i], + user_id=self.user_id_list[i], episode_append_id=self.episode_append_id_list[i], own_num=self.own_num_list[i], )) @@ -974,8 +993,9 @@ class SaoGetPartyDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party - def __init__(self, cmd) -> None: + def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: super().__init__(cmd) + self.result = 1 self.party_data_list_size = 1 # Number of arrays @@ -985,36 +1005,36 @@ class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party self.user_party_team_id_1 = "0" self.arrangement_num_1 = 0 - self.user_hero_log_id_1 = "101000010" - self.main_weapon_user_equipment_id_1 = "101000016" - self.sub_equipment_user_equipment_id_1 = "0" - self.skill_slot1_skill_id_1 = 30086 - self.skill_slot2_skill_id_1 = 1001 - self.skill_slot3_skill_id_1 = 1002 - self.skill_slot4_skill_id_1 = 1003 - self.skill_slot5_skill_id_1 = 1005 + self.user_hero_log_id_1 = str(hero1_data[2]) + self.main_weapon_user_equipment_id_1 = str(hero1_data[5]) + self.sub_equipment_user_equipment_id_1 = str(hero1_data[6]) + self.skill_slot1_skill_id_1 = hero1_data[7] + self.skill_slot2_skill_id_1 = hero1_data[8] + self.skill_slot3_skill_id_1 = hero1_data[9] + self.skill_slot4_skill_id_1 = hero1_data[10] + self.skill_slot5_skill_id_1 = hero1_data[11] self.user_party_team_id_2 = "0" self.arrangement_num_2 = 0 - self.user_hero_log_id_2 = "102000010" - self.main_weapon_user_equipment_id_2 = "103000006" - self.sub_equipment_user_equipment_id_2 = "0" - self.skill_slot1_skill_id_2 = 30086 - self.skill_slot2_skill_id_2 = 1001 - self.skill_slot3_skill_id_2 = 1002 - self.skill_slot4_skill_id_2 = 1003 - self.skill_slot5_skill_id_2 = 1005 + self.user_hero_log_id_2 = str(hero2_data[2]) + self.main_weapon_user_equipment_id_2 = str(hero2_data[5]) + self.sub_equipment_user_equipment_id_2 = str(hero2_data[6]) + self.skill_slot1_skill_id_2 = hero2_data[7] + self.skill_slot2_skill_id_2 = hero2_data[8] + self.skill_slot3_skill_id_2 = hero2_data[9] + self.skill_slot4_skill_id_2 = hero2_data[10] + self.skill_slot5_skill_id_2 = hero2_data[11] self.user_party_team_id_3 = "0" self.arrangement_num_3 = 0 - self.user_hero_log_id_3 = "103000010" - self.main_weapon_user_equipment_id_3 = "112000009" - self.sub_equipment_user_equipment_id_3 = "0" - self.skill_slot1_skill_id_3 = 30086 - self.skill_slot2_skill_id_3 = 1001 - self.skill_slot3_skill_id_3 = 1002 - self.skill_slot4_skill_id_3 = 1003 - self.skill_slot5_skill_id_3 = 1005 + self.user_hero_log_id_3 = str(hero3_data[2]) + self.main_weapon_user_equipment_id_3 = str(hero3_data[5]) + self.sub_equipment_user_equipment_id_3 = str(hero3_data[6]) + self.skill_slot1_skill_id_3 = hero3_data[7] + self.skill_slot2_skill_id_3 = hero3_data[8] + self.skill_slot3_skill_id_3 = hero3_data[9] + self.skill_slot4_skill_id_3 = hero3_data[10] + self.skill_slot5_skill_id_3 = hero3_data[11] def make(self) -> bytes: # create a resp struct diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py index b4fede2..3e75fc0 100644 --- a/titles/sao/schema/__init__.py +++ b/titles/sao/schema/__init__.py @@ -1,2 +1,3 @@ from .profile import SaoProfileData -from .static import SaoStaticData \ No newline at end of file +from .static import SaoStaticData +from .item import SaoItemData \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py new file mode 100644 index 0000000..a0d5123 --- /dev/null +++ b/titles/sao/schema/item.py @@ -0,0 +1,161 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +hero_log_data = Table( + "sao_hero_log_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_hero_log_id", Integer, nullable=False), + Column("log_level", Integer, nullable=False), + Column("log_exp", Integer, nullable=False), + Column("main_weapon", Integer, nullable=False), + Column("sub_equipment", Integer, nullable=False), + Column("skill_slot1_skill_id", Integer, nullable=False), + Column("skill_slot2_skill_id", Integer, nullable=False), + Column("skill_slot3_skill_id", Integer, nullable=False), + Column("skill_slot4_skill_id", Integer, nullable=False), + Column("skill_slot5_skill_id", Integer, nullable=False), + Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_hero_log_id", name="sao_hero_log_data_uk"), + mysql_charset="utf8mb4", +) + +hero_party = Table( + "sao_hero_party", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_party_team_id", Integer, nullable=False), + Column("user_hero_log_id_1", Integer, nullable=False), + Column("user_hero_log_id_2", Integer, nullable=False), + Column("user_hero_log_id_3", Integer, nullable=False), + UniqueConstraint("user", "user_party_team_id", name="sao_hero_party_uk"), + mysql_charset="utf8mb4", +) + +class SaoItemData(BaseData): + def put_hero_log(self, user_id: int, user_hero_log_id: int, log_level: int, log_exp: int, main_weapon: int, sub_equipment: int, skill_slot1_skill_id: int, skill_slot2_skill_id: int, skill_slot3_skill_id: int, skill_slot4_skill_id: int, skill_slot5_skill_id: int) -> Optional[int]: + sql = insert(hero_log_data).values( + user=user_id, + user_hero_log_id=user_hero_log_id, + log_level=log_level, + log_exp=log_exp, + main_weapon=main_weapon, + sub_equipment=sub_equipment, + skill_slot1_skill_id=skill_slot1_skill_id, + skill_slot2_skill_id=skill_slot2_skill_id, + skill_slot3_skill_id=skill_slot3_skill_id, + skill_slot4_skill_id=skill_slot4_skill_id, + skill_slot5_skill_id=skill_slot5_skill_id, + ) + + conflict = sql.on_duplicate_key_update( + log_level=hero_log_data.c.log_level, + log_exp=hero_log_data.c.log_exp, + main_weapon=hero_log_data.c.main_weapon, + sub_equipment=hero_log_data.c.sub_equipment, + skill_slot1_skill_id=hero_log_data.c.skill_slot1_skill_id, + skill_slot2_skill_id=hero_log_data.c.skill_slot2_skill_id, + skill_slot3_skill_id=hero_log_data.c.skill_slot3_skill_id, + skill_slot4_skill_id=hero_log_data.c.skill_slot4_skill_id, + skill_slot5_skill_id=hero_log_data.c.skill_slot5_skill_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero! user: {user_id}, user_hero_log_id: {user_hero_log_id}" + ) + return None + + return result.lastrowid + + def put_hero_party(self, user_id: int, user_party_team_id: int, user_hero_log_id_1: int, user_hero_log_id_2: int, user_hero_log_id_3: int) -> Optional[int]: + sql = insert(hero_party).values( + user=user_id, + user_party_team_id=user_party_team_id, + user_hero_log_id_1=user_hero_log_id_1, + user_hero_log_id_2=user_hero_log_id_2, + user_hero_log_id_3=user_hero_log_id_3, + ) + + conflict = sql.on_duplicate_key_update( + user_hero_log_id_1=hero_party.c.user_hero_log_id_1, + user_hero_log_id_2=hero_party.c.user_hero_log_id_2, + user_hero_log_id_3=hero_party.c.user_hero_log_id_3, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero party! user: {user_id}, user_party_team_id: {user_party_team_id}" + ) + return None + + return result.lastrowid + + def get_hero_log( + self, user_id: int, user_hero_log_id: int = None + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + hero_log_data.c.user_hero_log_id == user_hero_log_id if user_hero_log_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_hero_logs( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_hero_party( + self, user_id: int, user_party_team_id: int = None + ) -> Optional[List[Row]]: + sql = hero_party.select( + and_( + hero_party.c.user == user_id, + hero_party.c.user_party_team_id == user_party_team_id if user_party_team_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file From 2b4ac0638997e11559756aa19a3816d478cabfc2 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 29 May 2023 19:21:26 -0400 Subject: [PATCH 233/589] adding more profile & hero saving stuff to SAO --- titles/sao/base.py | 56 ++++++++++++++++++++++++++- titles/sao/schema/item.py | 75 ++++++++++++++++++++++++++++++------ titles/sao/schema/profile.py | 32 +++++++++++++++ 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 685d330..609c5ac 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -329,10 +329,18 @@ class SaoBase: user_id = req_data.user_id profile_data = self.game_data.profile.get_profile(user_id) + self.game_data.item.create_session( + user_id, + int(req_data.play_start_request_data[0].user_party_id), + req_data.episode_id, + req_data.play_mode, + req_data.play_start_request_data[0].quest_drop_boost_apply_flag + ) + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) return resp.make() - def handle_c908(self, request: Any) -> bytes: + def handle_c908(self, request: Any) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end req = bytes.fromhex(request)[24:] @@ -424,6 +432,52 @@ class SaoBase: req_data = req_struct.parse(req) + # Update the profile + profile = self.game_data.profile.get_profile(req_data.user_id) + + exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason + col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + + updated_profile = self.game_data.profile.put_profile( + req_data.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + exp, + col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Update heroes from the used party + play_session = self.game_data.item.get_session(req_data.user_id) + session_party = self.game_data.item.get_hero_party(req_data.user_id, play_session["user_party_team_id"]) + + hero_list = [] + hero_list.append(session_party["user_hero_log_id_1"]) + hero_list.append(session_party["user_hero_log_id_2"]) + hero_list.append(session_party["user_hero_log_id_3"]) + + for i in range(0,len(hero_list)): + hero_data = self.game_data.item.get_hero_log(req_data.user_id, hero_list[i]) + + log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + + self.game_data.item.put_hero_log( + req_data.user_id, + hero_data["user_hero_log_id"], + hero_data["log_level"], + log_exp, + hero_data["main_weapon"], + hero_data["sub_equipment"], + hero_data["skill_slot1_skill_id"], + hero_data["skill_slot2_skill_id"], + hero_data["skill_slot3_skill_id"], + hero_data["skill_slot4_skill_id"], + hero_data["skill_slot5_skill_id"] + ) + resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index a0d5123..f6d030e 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -49,7 +49,42 @@ hero_party = Table( mysql_charset="utf8mb4", ) +sessions = Table( + "sao_play_sessions", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_party_team_id", Integer, nullable=False), + Column("episode_id", Integer, nullable=False), + Column("play_mode", Integer, nullable=False), + Column("quest_drop_boost_apply_flag", Integer, nullable=False), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_party_team_id", "play_date", name="sao_play_sessions_uk"), + mysql_charset="utf8mb4", +) + class SaoItemData(BaseData): + def create_session(self, user_id: int, user_party_team_id: int, episode_id: int, play_mode: int, quest_drop_boost_apply_flag: int) -> Optional[int]: + sql = insert(sessions).values( + user=user_id, + user_party_team_id=user_party_team_id, + episode_id=episode_id, + play_mode=play_mode, + quest_drop_boost_apply_flag=quest_drop_boost_apply_flag + ) + + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO session for user {user_id}!") + return None + return result.lastrowid + def put_hero_log(self, user_id: int, user_hero_log_id: int, log_level: int, log_exp: int, main_weapon: int, sub_equipment: int, skill_slot1_skill_id: int, skill_slot2_skill_id: int, skill_slot3_skill_id: int, skill_slot4_skill_id: int, skill_slot5_skill_id: int) -> Optional[int]: sql = insert(hero_log_data).values( user=user_id, @@ -66,15 +101,15 @@ class SaoItemData(BaseData): ) conflict = sql.on_duplicate_key_update( - log_level=hero_log_data.c.log_level, - log_exp=hero_log_data.c.log_exp, - main_weapon=hero_log_data.c.main_weapon, - sub_equipment=hero_log_data.c.sub_equipment, - skill_slot1_skill_id=hero_log_data.c.skill_slot1_skill_id, - skill_slot2_skill_id=hero_log_data.c.skill_slot2_skill_id, - skill_slot3_skill_id=hero_log_data.c.skill_slot3_skill_id, - skill_slot4_skill_id=hero_log_data.c.skill_slot4_skill_id, - skill_slot5_skill_id=hero_log_data.c.skill_slot5_skill_id, + log_level=log_level, + log_exp=log_exp, + main_weapon=main_weapon, + sub_equipment=sub_equipment, + skill_slot1_skill_id=skill_slot1_skill_id, + skill_slot2_skill_id=skill_slot2_skill_id, + skill_slot3_skill_id=skill_slot3_skill_id, + skill_slot4_skill_id=skill_slot4_skill_id, + skill_slot5_skill_id=skill_slot5_skill_id, ) result = self.execute(conflict) @@ -96,9 +131,9 @@ class SaoItemData(BaseData): ) conflict = sql.on_duplicate_key_update( - user_hero_log_id_1=hero_party.c.user_hero_log_id_1, - user_hero_log_id_2=hero_party.c.user_hero_log_id_2, - user_hero_log_id_3=hero_party.c.user_hero_log_id_3, + user_hero_log_id_1=user_hero_log_id_1, + user_hero_log_id_2=user_hero_log_id_2, + user_hero_log_id_3=user_hero_log_id_3, ) result = self.execute(conflict) @@ -155,6 +190,22 @@ class SaoItemData(BaseData): ) ) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = sessions.select( + and_( + sessions.c.user == user_id, + ) + ).order_by( + sessions.c.play_date.asc() + ) + result = self.execute(sql) if result is None: return None diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py index 6ae60b1..b125717 100644 --- a/titles/sao/schema/profile.py +++ b/titles/sao/schema/profile.py @@ -40,6 +40,38 @@ class SaoProfileData(BaseData): return None return result.lastrowid + def put_profile(self, user_id: int, user_type: int, nick_name: str, rank_num: int, rank_exp: int, own_col: int, own_vp: int, own_yui_medal: int, setting_title_id: int) -> Optional[int]: + sql = insert(profile).values( + user=user_id, + user_type=user_type, + nick_name=nick_name, + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + conflict = sql.on_duplicate_key_update( + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert profile! user: {user_id}" + ) + return None + + print(result.lastrowid) + return result.lastrowid + def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = self.execute(sql) From a2fe11d654553cdee1276b2c3e68ee436594d099 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 29 May 2023 20:57:02 -0400 Subject: [PATCH 234/589] Fixing level calculation saving & loading on SAO --- titles/sao/base.py | 22 ++- titles/sao/data/HeroLogLevel.csv | 121 +++++++++++++ titles/sao/data/PlayerRank.csv | 301 +++++++++++++++++++++++++++++++ titles/sao/handlers/base.py | 29 ++- 4 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 titles/sao/data/HeroLogLevel.csv create mode 100644 titles/sao/data/PlayerRank.csv diff --git a/titles/sao/base.py b/titles/sao/base.py index 609c5ac..9626647 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -3,6 +3,7 @@ import json, logging from typing import Any, Dict import random import struct +import csv from core.data import Data from core import CoreConfig @@ -29,7 +30,6 @@ class SaoBase: def handle_c122(self, request: Any) -> bytes: #common/get_maintenance_info - resp = SaoGetMaintResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -438,11 +438,29 @@ class SaoBase: exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/PlayerRank.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for i in range(0,len(data)): + if exp>=int(data[i][1]) and exp None: @@ -493,9 +494,29 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): self.last_set_skill_slot5_skill_id = [] for i in range(len(hero_data)): + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = hero_data[i][4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp Date: Tue, 30 May 2023 12:14:18 +0200 Subject: [PATCH 235/589] cm: Added individual Card Maker version and maimai DX passes working --- core/data/schema/versions/SDEZ_4_rollback.sql | 3 ++ core/data/schema/versions/SDEZ_5_upgrade.sql | 3 ++ docs/game_specific_info.md | 53 +++++++++++++++---- example_config/cardmaker.yaml | 10 ++++ readme.md | 2 +- titles/cm/base.py | 38 ++++++++++--- titles/cm/cm135.py | 22 +------- titles/cm/config.py | 16 ++++++ titles/cm/const.py | 2 +- titles/cm/index.py | 4 +- titles/mai2/__init__.py | 2 +- titles/mai2/schema/item.py | 12 +++-- titles/mai2/universe.py | 34 ++++++++++-- 13 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_4_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_5_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_4_rollback.sql b/core/data/schema/versions/SDEZ_4_rollback.sql new file mode 100644 index 0000000..b8be7b3 --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE mai2_item_card + CHANGE COLUMN startDate startDate TIMESTAMP DEFAULT "2018-01-01 00:00:00.0", + CHANGE COLUMN endDate endDate TIMESTAMP DEFAULT "2038-01-01 00:00:00.0"; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_5_upgrade.sql b/core/data/schema/versions/SDEZ_5_upgrade.sql new file mode 100644 index 0000000..cc4912f --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE mai2_item_card + CHANGE COLUMN startDate startDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHANGE COLUMN endDate endDate TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 5361ce9..aa0a39f 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -253,13 +253,13 @@ python dbutils.py --game SDDT upgrade | Version ID | Version Name | |------------|-----------------| -| 0 | Card Maker 1.34 | +| 0 | Card Maker 1.30 | | 1 | Card Maker 1.35 | ### Support status -* Card Maker 1.34: +* Card Maker 1.30: * CHUNITHM NEW!!: Yes * maimai DX UNiVERSE: Yes * O.N.G.E.K.I. Bright: Yes @@ -285,19 +285,46 @@ python read.py --series SDED --version --binfolder titles/cm/cm_dat 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: +Also make sure to import all maimai DX 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 +The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/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 +### Config setup + +Make sure to update your `config/cardmaker.yaml` with the correct version for each game. To get the current version required to run a specific game, open every opt (Axxx) folder descending until you find all three folders: + +- `MU3`: O.N.G.E.K.I. +- `MAI`: maimai DX +- `CHU`: CHUNITHM + +Inside each folder is a `DataConfig.xml` file, for example: + +`MU3/DataConfig.xml`: +```xml + + 1 + 35 + 3 + +``` + +Now update your `config/cardmaker.yaml` with the correct version number, for example: + +```yaml +version: + 1: # Card Maker 1.35 + ongeki: 1.35.03 +``` + +### O.N.G.E.K.I. Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of getting an SSR card @@ -310,20 +337,24 @@ 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 +### CHUNITHM -All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards +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. +### maimai DX + +Printed maimai DX cards: Freedom (`cardTypeId=6`) or Gold Pass (`cardTypeId=4`) can now be selected during the login process. You can only have ONE Freedom and ONE Gold Pass active at a given time. The cards will expire after 15 days. + +Thanks GetzeAvenue for the `selectedCardList` rarity hint! + ### 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. +Card Maker 1.30-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. +The gachas inside the `config/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 diff --git a/example_config/cardmaker.yaml b/example_config/cardmaker.yaml index a04dda5..fb17756 100644 --- a/example_config/cardmaker.yaml +++ b/example_config/cardmaker.yaml @@ -1,3 +1,13 @@ server: enable: True loglevel: "info" + +version: + 0: + ongeki: 1.30.01 + chuni: 2.00.00 + maimai: 1.20.00 + 1: + ongeki: 1.35.03 + chuni: 2.10.00 + maimai: 1.30.00 \ No newline at end of file diff --git a/readme.md b/readme.md index ee407cd..f114bb0 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + All versions + Card Maker - + 1.34 + + 1.30 + 1.35 + O.N.G.E.K.I. diff --git a/titles/cm/base.py b/titles/cm/base.py index ff38489..dae6ecb 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -23,19 +23,40 @@ class CardMakerBase: self.game = CardMakerConstants.GAME_CODE self.version = CardMakerConstants.VER_CARD_MAKER + @staticmethod + def _parse_int_ver(version: str) -> str: + return version.replace(".", "")[:3] + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: 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}" - # CHUNITHM = 0, maimai = 1, ONGEKI = 2 + # grab the dict with all games version numbers from user config + games_ver = self.game_cfg.version.version(self.version) + 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/"}, + # CHUNITHM + { + "modelKind": 0, + "type": 1, + "titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/", + }, + # maimai DX + { + "modelKind": 1, + "type": 1, + "titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/", + }, + # ONGEKI + { + "modelKind": 2, + "type": 1, + "titleUri": f"{uri}/SDDT/{self._parse_int_ver(games_ver['ongeki'])}/", + }, ], } @@ -47,12 +68,15 @@ class CardMakerBase: datetime.now() + timedelta(hours=4), self.date_time_format ) + # grab the dict with all games version numbers from user config + games_ver = self.game_cfg.version.version(self.version) + return { "gameSetting": { "dataVersion": "1.30.00", - "ongekiCmVersion": "1.30.01", - "chuniCmVersion": "2.00.00", - "maimaiCmVersion": "1.20.00", + "ongekiCmVersion": games_ver["ongeki"], + "chuniCmVersion": games_ver["chuni"], + "maimaiCmVersion": games_ver["maimai"], "requestInterval": 10, "rebootStartTime": reboot_start, "rebootEndTime": reboot_end, diff --git a/titles/cm/cm135.py b/titles/cm/cm135.py index 782f07a..e134974 100644 --- a/titles/cm/cm135.py +++ b/titles/cm/cm135.py @@ -1,8 +1,4 @@ -from datetime import date, datetime, timedelta -from typing import Any, Dict, List -import json -import logging -from enum import Enum +from typing import Dict from core.config import CoreConfig from core.data.cache import cached @@ -16,23 +12,7 @@ class CardMaker135(CardMakerBase): super().__init__(core_cfg, game_cfg) 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) - 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/" - - 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.03" - ret["gameSetting"]["chuniCmVersion"] = "2.05.00" - ret["gameSetting"]["maimaiCmVersion"] = "1.25.00" return ret diff --git a/titles/cm/config.py b/titles/cm/config.py index ea96ca1..8bb23ec 100644 --- a/titles/cm/config.py +++ b/titles/cm/config.py @@ -1,3 +1,4 @@ +from typing import Dict from core.config import CoreConfig @@ -20,6 +21,21 @@ class CardMakerServerConfig: ) +class CardMakerVersionConfig: + def __init__(self, parent_config: "CardMakerConfig") -> None: + self.__config = parent_config + + def version(self, version: int) -> Dict: + """ + in the form of: + 1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00} + """ + return CoreConfig.get_config_field( + self.__config, "cardmaker", "version", default={} + )[version] + + class CardMakerConfig(dict): def __init__(self) -> None: self.server = CardMakerServerConfig(self) + self.version = CardMakerVersionConfig(self) diff --git a/titles/cm/const.py b/titles/cm/const.py index 09f289e..5bb6d1f 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -6,7 +6,7 @@ class CardMakerConstants: VER_CARD_MAKER = 0 VER_CARD_MAKER_135 = 1 - VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35") + VERSION_NAMES = ("Card Maker 1.30", "Card Maker 1.35") @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/cm/index.py b/titles/cm/index.py index 74d3a0d..3bde49c 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -30,7 +30,7 @@ class CardMakerServlet: self.versions = [ CardMakerBase(core_cfg, self.game_cfg), - CardMaker135(core_cfg, self.game_cfg), + CardMaker135(core_cfg, self.game_cfg) ] self.logger = logging.getLogger("cardmaker") @@ -89,7 +89,7 @@ class CardMakerServlet: if version >= 130 and version < 135: # Card Maker internal_ver = CardMakerConstants.VER_CARD_MAKER - elif version >= 135 and version < 136: # Card Maker 1.35 + elif version >= 135 and version < 140: # 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: diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 810eac9..c9d7db2 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 = 4 +current_schema_version = 5 diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 6280bbb..6b70ed1 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -39,8 +39,8 @@ card = Table( 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"), + Column("startDate", TIMESTAMP, nullable=False, server_default=func.now()), + Column("endDate", TIMESTAMP, nullable=False), UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), mysql_charset="utf8mb4", ) @@ -444,6 +444,8 @@ class Mai2ItemData(BaseData): card_kind: int, chara_id: int, map_id: int, + start_date: datetime, + end_date: datetime, ) -> Optional[Row]: sql = insert(card).values( user=user_id, @@ -451,9 +453,13 @@ class Mai2ItemData(BaseData): cardTypeId=card_kind, charaId=chara_id, mapId=map_id, + startDate=start_date, + endDate=end_date, ) - conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id) + conflict = sql.on_duplicate_key_update( + charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date + ) result = self.execute(conflict) if result is None: diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 56b3e8f..7cbd159 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -104,8 +104,12 @@ class Mai2Universe(Mai2Base): 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") + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) card_list.append(tmp) return { @@ -154,6 +158,10 @@ class Mai2Universe(Mai2Base): # set a random card serial number serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + user_card = upsert["userCard"] self.data.item.put_card( user_id, @@ -161,8 +169,26 @@ class Mai2Universe(Mai2Base): user_card["cardTypeId"], user_card["charaId"], user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, ) + # get the profile extend to save the new bought card + extend = self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: list = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + self.data.profile.put_profile_extend(user_id, self.version, extend) + # properly format userPrintDetail for the database upsert.pop("userCard") upsert.pop("serialId") @@ -174,8 +200,8 @@ class Mai2Universe(Mai2Base): "returnCode": 1, "orderId": 0, "serialId": serial_id, - "startDate": "2018-01-01 00:00:00", - "endDate": "2038-01-01 00:00:00", + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), } def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: From e466ddce551dcc664017a4b3fdd110ed57e7d93a Mon Sep 17 00:00:00 2001 From: Midorica Date: Tue, 30 May 2023 14:29:50 -0400 Subject: [PATCH 236/589] Adding SAO rewards saving for heroes --- titles/sao/base.py | 25 +- titles/sao/data/RewardTable.csv | 17274 ++++++++++++++++++++++++++++++ titles/sao/index.py | 1 - titles/sao/schema/static.py | 8 + 4 files changed, 17305 insertions(+), 3 deletions(-) create mode 100644 titles/sao/data/RewardTable.csv diff --git a/titles/sao/base.py b/titles/sao/base.py index 9626647..62fa367 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -3,7 +3,9 @@ import json, logging from typing import Any, Dict import random import struct -import csv +from csv import * +from random import choice +import random as rand from core.data import Data from core import CoreConfig @@ -495,6 +497,25 @@ class SaoBase: hero_data["skill_slot4_skill_id"], hero_data["skill_slot5_skill_id"] ) + + # Generate random hero(es) based off the response + for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): + + with open('titles/sao/data/RewardTable.csv', 'r') as f: + keys_unanalyzed = next(f).strip().split(',') + data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + + randomized_unanalyzed_id = choice(data_unanalyzed) + while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: + randomized_unanalyzed_id = choice(data_unanalyzed) + + heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + if heroList: + self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + + # Item and Equipments saving will be done later here + + # Send response resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -511,4 +532,4 @@ class SaoBase: def handle_c90a(self, request: Any) -> bytes: #should be tweaked for proper item unlock #quest/episode_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) - return resp.make() + return resp.make() \ No newline at end of file diff --git a/titles/sao/data/RewardTable.csv b/titles/sao/data/RewardTable.csv new file mode 100644 index 0000000..acce5bd --- /dev/null +++ b/titles/sao/data/RewardTable.csv @@ -0,0 +1,17274 @@ +RewardTableId,UnanalyzedLogGradeId,CommonRewardId +142,1,101000000 +143,2,101000001 +144,3,101000002 +145,4,101000008 +146,5,101000004 +147,1,102000000 +148,2,102000001 +149,3,102000002 +150,4,102000003 +151,5,102000004 +152,1,103000000 +153,2,103000001 +154,3,103000002 +155,4,103000003 +156,5,103000004 +157,1,105000000 +158,2,105000001 +159,3,105000002 +160,4,105000003 +161,5,105000004 +162,1,108000000 +163,2,108000001 +164,3,108000002 +165,4,108000003 +166,5,108000004 +167,1,109000000 +168,2,109000001 +169,3,109000002 +170,4,109000003 +171,5,109000004 +172,1,111000000 +173,2,111000001 +174,3,111000002 +175,4,111000003 +176,5,111000004 +177,1,112000000 +178,2,112000001 +179,3,112000002 +180,4,112000003 +181,5,112000004 +182,1,120000000 +183,2,120000001 +184,3,120000002 +185,4,120000003 +186,5,120000004 +187,1,101000010 +188,2,101000030 +189,3,101000040 +190,1,102000010 +191,2,102000030 +192,3,102000070 +193,1,103000010 +194,2,103000040 +195,3,103000070 +196,1,104000010 +197,2,104000030 +198,3,104000070 +199,1,105000010 +200,2,105000040 +201,3,105000070 +202,1,106000010 +203,2,106000030 +204,3,106000070 +205,1,107000010 +206,2,107000040 +207,3,107000070 +208,1,108000010 +209,2,108000030 +210,3,108000050 +212,1,101000000 +213,2,101000001 +214,3,101000002 +215,4,101000008 +216,5,101000004 +218,1,101000000 +219,2,101000001 +220,3,101000002 +221,4,101000008 +222,5,101000004 +223,1,102000000 +224,2,102000001 +225,3,102000002 +226,4,102000003 +227,5,102000004 +228,1,103000000 +229,2,103000001 +230,3,103000002 +231,4,103000003 +232,5,103000004 +233,1,105000000 +234,2,105000001 +235,3,105000002 +236,4,105000003 +237,5,105000004 +238,1,108000000 +239,2,108000001 +240,3,108000002 +241,4,108000003 +242,5,108000004 +243,1,109000000 +244,2,109000001 +245,3,109000002 +246,4,109000003 +247,5,109000004 +248,1,111000000 +249,2,111000001 +250,3,111000002 +251,4,111000003 +252,5,111000004 +253,1,112000000 +254,2,112000001 +255,3,112000002 +256,4,112000003 +257,5,112000004 +258,1,120000000 +259,2,120000001 +260,3,120000002 +261,4,120000003 +262,5,120000004 +263,1,101000010 +264,2,101000020 +265,2,101000030 +266,3,101000040 +267,3,101000050 +268,3,101000060 +269,3,101000070 +270,3,101000080 +271,1,102000010 +272,2,102000020 +273,2,102000030 +274,2,102000040 +275,3,102000050 +276,3,102000060 +277,3,102000070 +278,3,102000080 +279,1,103000010 +280,2,103000020 +281,2,103000030 +282,2,103000040 +283,3,103000050 +284,3,103000060 +285,3,103000070 +286,3,103000080 +287,1,104000010 +288,2,104000020 +289,2,104000030 +290,2,104000040 +291,3,104000050 +292,3,104000060 +293,3,104000070 +294,3,104000080 +295,1,105000010 +296,2,105000020 +297,2,105000030 +298,2,105000040 +299,3,105000050 +300,3,105000060 +301,3,105000070 +302,3,105000080 +303,1,106000010 +304,2,106000020 +305,2,106000030 +306,2,106000040 +307,3,106000050 +308,3,106000060 +309,3,106000070 +310,3,106000080 +311,1,107000010 +312,2,107000020 +313,2,107000030 +314,2,107000040 +315,3,107000050 +316,3,107000060 +317,3,107000070 +318,3,107000080 +319,1,108000010 +320,2,108000020 +321,2,108000030 +322,2,108000040 +323,3,108000050 +324,3,108000060 +325,3,108000070 +326,3,108000080 +327,2,180000 +329,1,101000000 +330,2,101000001 +331,3,101000002 +332,4,101000008 +333,5,101000004 +334,1,102000000 +335,2,102000001 +336,3,102000002 +337,4,102000003 +338,5,102000004 +339,1,103000000 +340,2,103000001 +341,3,103000002 +342,4,103000003 +343,5,103000004 +344,1,105000000 +345,2,105000001 +346,3,105000002 +347,4,105000003 +348,5,105000004 +349,1,108000000 +350,2,108000001 +351,3,108000002 +352,4,108000003 +353,5,108000004 +354,1,109000000 +355,2,109000001 +356,3,109000002 +357,4,109000003 +358,5,109000004 +359,1,111000000 +360,2,111000001 +361,3,111000002 +362,4,111000003 +363,5,111000004 +364,1,112000000 +365,2,112000001 +366,3,112000002 +367,4,112000003 +368,5,112000004 +369,1,120000000 +370,2,120000001 +371,3,120000002 +372,4,120000003 +373,5,120000004 +374,1,101000010 +375,2,101000020 +376,2,101000030 +377,3,101000040 +378,3,101000050 +379,3,101000060 +380,3,101000070 +381,3,101000080 +382,1,102000010 +383,2,102000020 +384,2,102000030 +385,2,102000040 +386,3,102000050 +387,3,102000060 +388,3,102000070 +389,3,102000080 +390,1,103000010 +391,2,103000020 +392,2,103000030 +393,2,103000040 +394,3,103000050 +395,3,103000060 +396,3,103000070 +397,3,103000080 +398,1,104000010 +399,2,104000020 +400,2,104000030 +401,2,104000040 +402,3,104000050 +403,3,104000060 +404,3,104000070 +405,3,104000080 +406,1,105000010 +407,2,105000020 +408,2,105000030 +409,2,105000040 +410,3,105000050 +411,3,105000060 +412,3,105000070 +413,3,105000080 +414,1,106000010 +415,2,106000020 +416,2,106000030 +417,2,106000040 +418,3,106000050 +419,3,106000060 +420,3,106000070 +421,3,106000080 +422,1,107000010 +423,2,107000020 +424,2,107000030 +425,2,107000040 +426,3,107000050 +427,3,107000060 +428,3,107000070 +429,3,107000080 +430,1,108000010 +431,2,108000020 +432,2,108000030 +433,2,108000040 +434,3,108000050 +435,3,108000060 +436,3,108000070 +437,3,108000080 +438,2,180000 +440,1,101000000 +441,2,101000001 +442,3,101000002 +443,4,101000008 +444,5,101000004 +446,1,102000000 +447,2,102000001 +448,3,102000002 +449,4,102000003 +450,5,102000004 +451,1,112000000 +452,2,112000001 +453,3,112000002 +454,4,112000003 +455,5,112000004 +457,1,101000000 +458,2,101000001 +459,3,101000002 +460,4,101000008 +461,5,101000004 +462,1,120000000 +463,2,120000001 +464,3,120000002 +465,4,120000003 +466,5,120000004 +468,1,111000000 +469,2,111000001 +470,3,111000002 +471,4,111000003 +472,5,111000004 +473,1,120000000 +474,2,120000001 +475,3,120000002 +476,4,120000003 +477,5,120000004 +479,1,109000000 +480,2,109000001 +481,3,109000002 +482,4,109000003 +483,5,109000004 +484,1,112000000 +485,2,112000001 +486,3,112000002 +487,4,112000003 +488,5,112000004 +490,1,103000000 +491,2,103000001 +492,3,103000002 +493,4,103000003 +494,5,103000004 +495,1,112000000 +496,2,112000001 +497,3,112000002 +498,4,112000003 +499,5,112000004 +501,1,105000000 +502,2,105000001 +503,3,105000002 +504,4,105000003 +505,5,105000004 +506,1,120000000 +507,2,120000001 +508,3,120000002 +509,4,120000003 +510,5,120000004 +512,1,108000000 +513,2,108000001 +514,3,108000002 +515,4,108000003 +516,5,108000004 +517,1,120000000 +518,2,120000001 +519,3,120000002 +520,4,120000003 +521,5,120000004 +523,1,101000010 +524,1,103000010 +525,2,103000030 +526,2,103000040 +527,2,107000020 +528,2,101000030 +529,3,101000050 +530,3,101000060 +531,3,101000080 +532,3,103000060 +533,3,103000070 +534,3,103000080 +535,3,101000040 +536,1,101000000 +537,2,101000001 +538,3,101000002 +539,4,101000008 +540,5,101000004 +541,1,112000000 +542,2,112000001 +543,3,112000002 +544,4,112000003 +545,5,112000004 +546,2,101000005 +547,2,112000005 +548,3,170000 +549,4,170001 +551,1,102000010 +552,2,102000030 +553,2,102000040 +554,2,104000020 +555,3,102000060 +556,3,102000070 +557,3,102000080 +558,3,103000050 +559,3,105000050 +560,1,102000000 +561,2,102000001 +562,3,102000002 +563,4,102000003 +564,5,102000004 +565,1,112000000 +566,2,112000001 +567,3,112000002 +568,4,112000003 +569,5,112000004 +570,3,170000 +571,4,170001 +573,1,106000010 +574,2,106000030 +575,2,106000040 +576,2,105000020 +577,3,106000060 +578,3,106000070 +579,3,106000080 +580,3,101000070 +581,1,103000000 +582,2,103000001 +583,3,103000002 +584,4,103000003 +585,5,103000004 +586,1,112000000 +587,2,112000001 +588,3,112000002 +589,4,112000003 +590,5,112000004 +591,3,170000 +592,4,170001 +594,1,104000010 +595,2,104000030 +596,2,104000040 +597,2,108000020 +598,3,104000060 +599,3,104000070 +600,3,104000080 +601,3,102000050 +602,1,111000000 +603,2,111000001 +604,3,111000002 +605,4,111000003 +606,5,111000004 +607,1,120000000 +608,2,120000001 +609,3,120000002 +610,4,120000003 +611,5,120000004 +612,1,110020 +613,1,110030 +614,1,110040 +615,1,110050 +616,3,110060 +617,3,110070 +618,3,110080 +619,3,110090 +620,3,110100 +621,3,110110 +622,3,110120 +623,3,110130 +624,3,110140 +625,3,110150 +626,3,110160 +627,3,110170 +628,3,110180 +629,3,110190 +630,3,110200 +631,3,110210 +632,3,110220 +633,3,110230 +634,3,110240 +635,3,110250 +636,3,110260 +637,3,110270 +639,1,107000010 +640,2,107000030 +641,2,107000040 +642,2,101000020 +643,3,107000060 +644,3,107000070 +645,3,107000080 +646,3,108000050 +647,1,105000000 +648,2,105000001 +649,3,105000002 +650,4,105000003 +651,5,105000004 +652,1,120000000 +653,2,120000001 +654,3,120000002 +655,4,120000003 +656,5,120000004 +657,3,180000 +658,4,180001 +660,1,105000010 +661,2,105000030 +662,2,105000040 +663,2,102000020 +664,2,103000020 +665,3,105000060 +666,3,105000070 +667,3,105000080 +668,3,104000050 +669,3,106000050 +670,1,109000000 +671,2,109000001 +672,3,109000002 +673,4,109000003 +674,5,109000004 +675,1,112000000 +676,2,112000001 +677,3,112000002 +678,4,112000003 +679,5,112000004 +680,1,120020 +681,1,120030 +682,1,120040 +683,1,120050 +684,3,120240 +685,3,120250 +686,3,120260 +687,3,120270 +688,3,120300 +689,3,120310 +690,3,120320 +691,3,120330 +692,3,120340 +693,3,120350 +694,3,120360 +695,3,120370 +696,3,120380 +697,3,120390 +698,3,120400 +699,3,120410 +700,3,120420 +701,3,120430 +702,3,120450 +703,3,120460 +705,1,108000010 +706,2,108000030 +707,2,108000040 +708,2,106000020 +709,3,108000060 +710,3,108000070 +711,3,108000080 +712,3,107000050 +713,1,108000000 +714,2,108000001 +715,3,108000002 +716,4,108000003 +717,5,108000004 +718,1,120000000 +719,2,120000001 +720,3,120000002 +721,4,120000003 +722,5,120000004 +723,1,130020 +724,1,130030 +725,1,130040 +726,1,130050 +727,3,130060 +728,3,130070 +729,3,130080 +730,3,130090 +731,3,130100 +732,3,130110 +733,3,130120 +734,3,130130 +735,3,130140 +736,3,130150 +737,3,130160 +738,3,130170 +739,3,130180 +740,3,130190 +741,3,130200 +742,3,130420 +743,3,130510 +744,3,130520 +745,3,130530 +746,3,130540 +748,1,105000010 +749,2,105000030 +750,2,105000040 +751,2,102000020 +752,2,103000020 +753,3,105000060 +754,3,105000070 +755,3,105000080 +756,3,104000050 +757,3,106000050 +758,1,109000000 +759,2,109000001 +760,3,109000002 +761,4,109000003 +762,5,109000004 +763,1,112000000 +764,2,112000001 +765,3,112000002 +766,4,112000003 +767,5,112000004 +768,3,170000 +769,4,170001 +771,1,104000010 +772,2,104000030 +773,2,104000040 +774,2,108000020 +775,3,104000060 +776,3,104000070 +777,3,104000080 +778,3,102000050 +779,1,111000000 +780,2,111000001 +781,3,111000002 +782,4,111000003 +783,5,111000004 +784,1,120000000 +785,2,120000001 +786,3,120000002 +787,4,120000003 +788,5,120000004 +789,1,110020 +790,1,110030 +791,1,110040 +792,1,110050 +793,3,110060 +794,3,110070 +795,3,110080 +796,3,110090 +797,3,110100 +798,3,110110 +799,3,110120 +800,3,110130 +801,3,110140 +802,3,110150 +803,3,110160 +804,3,110170 +805,3,110180 +806,3,110190 +807,3,110200 +808,3,110210 +809,3,110220 +810,3,110230 +811,3,110240 +812,3,110250 +813,3,110260 +814,3,110270 +816,1,102000010 +817,2,102000030 +818,2,102000040 +819,2,104000020 +820,3,102000060 +821,3,102000070 +822,3,102000080 +823,3,103000050 +824,3,105000050 +825,1,102000000 +826,2,102000001 +827,3,102000002 +828,4,102000003 +829,5,102000004 +830,1,112000000 +831,2,112000001 +832,3,112000002 +833,4,112000003 +834,5,112000004 +835,3,170001 +836,4,170002 +838,1,102000010 +839,2,102000030 +840,2,102000040 +841,2,104000020 +842,3,102000060 +843,3,102000070 +844,3,102000080 +845,3,103000050 +846,3,105000050 +847,1,102000000 +848,2,102000001 +849,3,102000002 +850,4,102000003 +851,5,102000004 +852,1,112000000 +853,2,112000001 +854,3,112000002 +855,4,112000003 +856,5,112000004 +857,1,110020 +858,1,110030 +859,1,110040 +860,1,110050 +861,2,110021 +862,2,110031 +863,2,110041 +864,2,110051 +865,3,110060 +866,3,110070 +867,3,110080 +868,3,110090 +869,3,110100 +870,3,110110 +871,3,110120 +872,3,110130 +873,3,110140 +874,3,110150 +875,3,110160 +876,3,110170 +877,3,110180 +878,3,110190 +879,3,110200 +880,3,110210 +881,3,110220 +882,3,110230 +883,3,110240 +884,3,110250 +885,3,110260 +886,3,110270 +887,4,140000 +889,1,101000010 +890,1,103000010 +891,2,103000030 +892,2,103000040 +893,2,107000020 +894,2,101000030 +895,3,101000050 +896,3,101000060 +897,3,101000080 +898,3,103000060 +899,3,103000070 +900,3,103000080 +901,3,101000040 +902,1,101000000 +903,2,101000001 +904,3,101000002 +905,4,101000008 +906,5,101000004 +907,1,112000000 +908,2,112000001 +909,3,112000002 +910,4,112000003 +911,5,112000004 +912,1,120020 +913,1,120030 +914,1,120040 +915,1,120050 +916,2,120021 +917,2,120031 +918,2,120041 +919,2,120051 +920,3,120240 +921,3,120250 +922,3,120260 +923,3,120270 +924,3,120300 +925,3,120310 +926,3,120320 +927,3,120330 +928,3,120340 +929,3,120350 +930,3,120360 +931,3,120370 +932,3,120380 +933,3,120390 +934,3,120400 +935,3,120410 +936,3,120420 +937,3,120430 +938,3,120450 +939,3,120460 +940,4,140000 +942,1,106000010 +943,2,106000030 +944,2,106000040 +945,2,105000020 +946,3,106000060 +947,3,106000070 +948,3,106000080 +949,3,101000070 +950,1,103000000 +951,2,103000001 +952,3,103000002 +953,4,103000003 +954,5,103000004 +955,1,112000000 +956,2,112000001 +957,3,112000002 +958,4,112000003 +959,5,112000004 +960,3,180001 +961,4,180002 +963,1,101000010 +964,1,103000010 +965,2,103000030 +966,2,103000040 +967,2,107000020 +968,2,101000030 +969,3,101000050 +970,3,101000060 +971,3,101000080 +972,3,103000060 +973,3,103000070 +974,3,103000080 +975,3,101000040 +976,1,101000000 +977,2,101000001 +978,3,101000002 +979,4,101000008 +980,5,101000004 +981,1,120000000 +982,2,120000001 +983,3,120000002 +984,4,120000003 +985,5,120000004 +986,3,170001 +987,4,170002 +989,1,105000010 +990,2,105000030 +991,2,105000040 +992,2,102000020 +993,2,103000020 +994,3,105000060 +995,3,105000070 +996,3,105000080 +997,3,104000050 +998,3,106000050 +999,1,109000000 +1000,2,109000001 +1001,3,109000002 +1002,4,109000003 +1003,5,109000004 +1004,1,112000000 +1005,2,112000001 +1006,3,112000002 +1007,4,112000003 +1008,5,112000004 +1009,1,130020 +1010,1,130030 +1011,1,130040 +1012,1,130050 +1013,2,130021 +1014,2,130031 +1015,2,130041 +1016,2,130051 +1017,3,130060 +1018,3,130070 +1019,3,130080 +1020,3,130090 +1021,3,130100 +1022,3,130110 +1023,3,130120 +1024,3,130130 +1025,3,130140 +1026,3,130150 +1027,3,130160 +1028,3,130170 +1029,3,130180 +1030,3,130190 +1031,3,130200 +1032,3,130420 +1033,3,130510 +1034,3,130520 +1035,3,130530 +1036,3,130540 +1037,4,140000 +1039,1,107000010 +1040,2,107000030 +1041,2,107000040 +1042,2,101000020 +1043,3,107000060 +1044,3,107000070 +1045,3,107000080 +1046,3,108000050 +1047,1,105000000 +1048,2,105000001 +1049,3,105000002 +1050,4,105000003 +1051,5,105000004 +1052,1,120000000 +1053,2,120000001 +1054,3,120000002 +1055,4,120000003 +1056,5,120000004 +1057,1,120020 +1058,1,120030 +1059,1,120040 +1060,1,120050 +1061,2,120021 +1062,2,120031 +1063,2,120041 +1064,2,120051 +1065,3,120240 +1066,3,120250 +1067,3,120260 +1068,3,120270 +1069,3,120300 +1070,3,120310 +1071,3,120320 +1072,3,120330 +1073,3,120340 +1074,3,120350 +1075,3,120360 +1076,3,120370 +1077,3,120380 +1078,3,120390 +1079,3,120400 +1080,3,120410 +1081,3,120420 +1082,3,120430 +1083,3,120450 +1084,3,120460 +1085,4,140000 +1087,1,108000010 +1088,2,108000030 +1089,2,108000040 +1090,2,106000020 +1091,3,108000060 +1092,3,108000070 +1093,3,108000080 +1094,3,107000050 +1095,1,108000000 +1096,2,108000001 +1097,3,108000002 +1098,4,108000003 +1099,5,108000004 +1100,1,120000000 +1101,2,120000001 +1102,3,120000002 +1103,4,120000003 +1104,5,120000004 +1105,3,170001 +1106,4,170002 +1108,1,101000010 +1109,1,103000010 +1110,2,103000030 +1111,2,103000040 +1112,2,107000020 +1113,2,101000030 +1114,3,101000050 +1115,3,101000060 +1116,3,101000080 +1117,3,103000060 +1118,3,103000070 +1119,3,103000080 +1120,3,101000040 +1121,1,101000000 +1122,2,101000001 +1123,3,101000002 +1124,4,101000008 +1125,5,101000004 +1126,1,112000000 +1127,2,112000001 +1128,3,112000002 +1129,4,112000003 +1130,5,112000004 +1131,2,101000005 +1132,2,112000005 +1133,3,170001 +1134,4,170002 +1136,1,102000010 +1137,2,102000030 +1138,2,102000040 +1139,2,104000020 +1140,3,102000060 +1141,3,102000070 +1142,3,102000080 +1143,3,103000050 +1144,3,105000050 +1145,1,102000000 +1146,2,102000001 +1147,3,102000002 +1148,4,102000003 +1149,5,102000004 +1150,1,112000000 +1151,2,112000001 +1152,3,112000002 +1153,4,112000003 +1154,5,112000004 +1155,1,120020 +1156,1,120030 +1157,1,120040 +1158,1,120050 +1159,2,120021 +1160,2,120031 +1161,2,120041 +1162,2,120051 +1163,3,120240 +1164,3,120250 +1165,3,120260 +1166,3,120270 +1167,3,120300 +1168,3,120310 +1169,3,120320 +1170,3,120330 +1171,3,120340 +1172,3,120350 +1173,3,120360 +1174,3,120370 +1175,3,120380 +1176,3,120390 +1177,3,120400 +1178,3,120410 +1179,3,120420 +1180,3,120430 +1181,3,120450 +1182,3,120460 +1183,4,140000 +1185,2,104000030 +1186,2,104000040 +1187,2,108000020 +1188,3,104000060 +1189,3,104000070 +1190,3,104000080 +1191,3,102000050 +1192,4,104000100 +1193,4,104000110 +1194,4,107000090 +1195,1,111000000 +1196,2,111000001 +1197,3,111000002 +1198,4,111000003 +1199,5,111000004 +1200,1,120000000 +1201,2,120000001 +1202,3,120000002 +1203,4,120000003 +1204,5,120000004 +1205,3,170002 +1206,4,170003 +1208,2,104000030 +1209,2,104000040 +1210,2,108000020 +1211,3,104000060 +1212,3,104000070 +1213,3,104000080 +1214,3,102000050 +1215,4,104000100 +1216,4,104000110 +1217,4,107000090 +1218,1,111000000 +1219,2,111000001 +1220,3,111000002 +1221,4,111000003 +1222,5,111000004 +1223,1,120000000 +1224,2,120000001 +1225,3,120000002 +1226,4,120000003 +1227,5,120000004 +1228,1,110020 +1229,1,110030 +1230,1,110040 +1231,1,110050 +1232,2,110021 +1233,2,110031 +1234,2,110041 +1235,2,110051 +1236,3,110022 +1237,3,110032 +1238,3,110042 +1239,3,110052 +1240,3,110060 +1241,3,110070 +1242,3,110080 +1243,3,110090 +1244,3,110100 +1245,3,110110 +1246,3,110120 +1247,3,110130 +1248,3,110140 +1249,3,110150 +1250,3,110160 +1251,3,110170 +1252,3,110180 +1253,3,110190 +1254,3,110200 +1255,3,110210 +1256,3,110220 +1257,3,110230 +1258,3,110240 +1259,3,110250 +1260,3,110260 +1261,3,110270 +1262,4,140000 +1264,2,105000030 +1265,2,105000040 +1266,2,102000020 +1267,2,103000020 +1268,3,105000060 +1269,3,105000070 +1270,3,105000080 +1271,3,104000050 +1272,3,106000050 +1273,4,105000100 +1274,4,105000110 +1275,4,108000090 +1276,1,109000000 +1277,2,109000001 +1278,3,109000002 +1279,4,109000003 +1280,5,109000004 +1281,1,112000000 +1282,2,112000001 +1283,3,112000002 +1284,4,112000003 +1285,5,112000004 +1286,3,170002 +1287,4,170003 +1289,2,106000030 +1290,2,106000040 +1291,2,105000020 +1292,3,106000060 +1293,3,106000070 +1294,3,106000080 +1295,3,101000070 +1296,4,106000100 +1297,4,106000110 +1298,4,104000090 +1299,1,103000000 +1300,2,103000001 +1301,3,103000002 +1302,4,103000003 +1303,5,103000004 +1304,1,112000000 +1305,2,112000001 +1306,3,112000002 +1307,4,112000003 +1308,5,112000004 +1309,1,130020 +1310,1,130030 +1311,1,130040 +1312,1,130050 +1313,2,130021 +1314,2,130031 +1315,2,130041 +1316,2,130051 +1317,3,130022 +1318,3,130032 +1319,3,130042 +1320,3,130052 +1321,3,130060 +1322,3,130070 +1323,3,130080 +1324,3,130090 +1325,3,130100 +1326,3,130110 +1327,3,130120 +1328,3,130130 +1329,3,130140 +1330,3,130150 +1331,3,130160 +1332,3,130170 +1333,3,130180 +1334,3,130190 +1335,3,130200 +1336,3,130420 +1337,3,130510 +1338,3,130520 +1339,3,130530 +1340,3,130540 +1341,4,140000 +1343,2,108000030 +1344,2,108000040 +1345,2,106000020 +1346,3,108000060 +1347,3,108000070 +1348,3,108000080 +1349,3,107000050 +1350,4,108000100 +1351,4,105000090 +1352,1,108000000 +1353,2,108000001 +1354,3,108000002 +1355,4,108000003 +1356,5,108000004 +1357,1,120000000 +1358,2,120000001 +1359,3,120000002 +1360,4,120000003 +1361,5,120000004 +1362,1,120020 +1363,1,120030 +1364,1,120040 +1365,1,120050 +1366,2,120021 +1367,2,120031 +1368,2,120041 +1369,2,120051 +1370,3,120022 +1371,3,120032 +1372,3,120042 +1373,3,120052 +1374,3,120240 +1375,3,120250 +1376,3,120260 +1377,3,120270 +1378,3,120300 +1379,3,120310 +1380,3,120320 +1381,3,120330 +1382,3,120340 +1383,3,120350 +1384,3,120360 +1385,3,120370 +1386,3,120380 +1387,3,120390 +1388,3,120400 +1389,3,120410 +1390,3,120420 +1391,3,120430 +1392,3,120450 +1393,3,120460 +1394,4,140000 +1396,2,103000030 +1397,2,103000040 +1398,2,107000020 +1399,2,101000030 +1400,3,101000050 +1401,3,101000060 +1402,3,101000080 +1403,3,103000060 +1404,3,103000070 +1405,3,103000080 +1406,3,101000040 +1407,4,101000090 +1408,4,102000090 +1409,4,103000100 +1410,4,101000100 +1411,4,101000110 +1412,1,101000000 +1413,2,101000001 +1414,3,101000002 +1415,4,101000008 +1416,5,101000004 +1417,1,120000000 +1418,2,120000001 +1419,3,120000002 +1420,4,120000003 +1421,5,120000004 +1422,3,170002 +1423,4,170003 +1425,2,105000030 +1426,2,105000040 +1427,2,102000020 +1428,2,103000020 +1429,3,105000060 +1430,3,105000070 +1431,3,105000080 +1432,3,104000050 +1433,3,106000050 +1434,4,105000100 +1435,4,105000110 +1436,4,108000090 +1437,1,109000000 +1438,2,109000001 +1439,3,109000002 +1440,4,109000003 +1441,5,109000004 +1442,1,112000000 +1443,2,112000001 +1444,3,112000002 +1445,4,112000003 +1446,5,112000004 +1447,3,180002 +1448,4,180003 +1450,2,107000030 +1451,2,107000040 +1452,2,101000020 +1453,3,107000060 +1454,3,107000070 +1455,3,107000080 +1456,3,108000050 +1457,4,107000100 +1458,4,103000090 +1459,1,105000000 +1460,2,105000001 +1461,3,105000002 +1462,4,105000003 +1463,5,105000004 +1464,1,120000000 +1465,2,120000001 +1466,3,120000002 +1467,4,120000003 +1468,5,120000004 +1469,3,170002 +1470,4,170003 +1472,2,104000030 +1473,2,104000040 +1474,2,108000020 +1475,3,104000060 +1476,3,104000070 +1477,3,104000080 +1478,3,102000050 +1479,4,104000100 +1480,4,104000110 +1481,4,107000090 +1482,1,111000000 +1483,2,111000001 +1484,3,111000002 +1485,4,111000003 +1486,5,111000004 +1487,1,120000000 +1488,2,120000001 +1489,3,120000002 +1490,4,120000003 +1491,5,120000004 +1492,1,110020 +1493,1,110030 +1494,1,110040 +1495,1,110050 +1496,2,110021 +1497,2,110031 +1498,2,110041 +1499,2,110051 +1500,3,110022 +1501,3,110032 +1502,3,110042 +1503,3,110052 +1504,3,110060 +1505,3,110070 +1506,3,110080 +1507,3,110090 +1508,3,110100 +1509,3,110110 +1510,3,110120 +1511,3,110130 +1512,3,110140 +1513,3,110150 +1514,3,110160 +1515,3,110170 +1516,3,110180 +1517,3,110190 +1518,3,110200 +1519,3,110210 +1520,3,110220 +1521,3,110230 +1522,3,110240 +1523,3,110250 +1524,3,110260 +1525,3,110270 +1526,4,140000 +1528,2,108000030 +1529,2,108000040 +1530,2,106000020 +1531,3,108000060 +1532,3,108000070 +1533,3,108000080 +1534,3,107000050 +1535,4,108000100 +1536,4,105000090 +1537,1,108000000 +1538,2,108000001 +1539,3,108000002 +1540,4,108000003 +1541,5,108000004 +1542,1,120000000 +1543,2,120000001 +1544,3,120000002 +1545,4,120000003 +1546,5,120000004 +1547,3,170002 +1548,4,170003 +1550,2,103000030 +1551,2,103000040 +1552,2,107000020 +1553,2,101000030 +1554,3,101000050 +1555,3,101000060 +1556,3,101000080 +1557,3,103000060 +1558,3,103000070 +1559,3,103000080 +1560,3,101000040 +1561,4,101000090 +1562,4,102000090 +1563,4,103000100 +1564,4,101000100 +1565,4,101000110 +1566,1,101000000 +1567,2,101000001 +1568,3,101000002 +1569,4,101000008 +1570,5,101000004 +1571,1,112000000 +1572,2,112000001 +1573,3,112000002 +1574,4,112000003 +1575,5,112000004 +1576,1,120020 +1577,1,120030 +1578,1,120040 +1579,1,120050 +1580,2,120021 +1581,2,120031 +1582,2,120041 +1583,2,120051 +1584,3,120022 +1585,3,120032 +1586,3,120042 +1587,3,120052 +1588,4,120023 +1589,4,120033 +1590,4,120043 +1591,4,120053 +1592,3,120240 +1593,3,120250 +1594,3,120260 +1595,3,120270 +1596,3,120300 +1597,3,120310 +1598,3,120320 +1599,3,120330 +1600,3,120340 +1601,3,120350 +1602,3,120360 +1603,3,120370 +1604,3,120380 +1605,3,120390 +1606,3,120400 +1607,3,120410 +1608,3,120420 +1609,3,120430 +1610,3,120450 +1611,3,120460 +1612,4,140000 +1613,4,150010 +1614,4,150020 +1615,4,150030 +1616,4,150040 +1618,2,102000030 +1619,2,102000040 +1620,2,104000020 +1621,3,102000060 +1622,3,102000070 +1623,3,102000080 +1624,3,103000050 +1625,3,105000050 +1626,4,102000100 +1627,4,102000110 +1628,4,106000090 +1629,1,102000000 +1630,2,102000001 +1631,3,102000002 +1632,4,102000003 +1633,5,102000004 +1634,1,112000000 +1635,2,112000001 +1636,3,112000002 +1637,4,112000003 +1638,5,112000004 +1639,1,110020 +1640,1,110030 +1641,1,110040 +1642,1,110050 +1643,2,110021 +1644,2,110031 +1645,2,110041 +1646,2,110051 +1647,3,110022 +1648,3,110032 +1649,3,110042 +1650,3,110052 +1651,4,110023 +1652,4,110033 +1653,4,110043 +1654,4,110053 +1655,3,110060 +1656,3,110070 +1657,3,110080 +1658,3,110090 +1659,3,110100 +1660,3,110110 +1661,3,110120 +1662,3,110130 +1663,3,110140 +1664,3,110150 +1665,3,110160 +1666,3,110170 +1667,3,110180 +1668,3,110190 +1669,3,110200 +1670,3,110210 +1671,3,110220 +1672,3,110230 +1673,3,110240 +1674,3,110250 +1675,3,110260 +1676,3,110270 +1677,4,140000 +1678,4,150010 +1679,4,150020 +1680,4,150030 +1681,4,150040 +1683,2,106000030 +1684,2,106000040 +1685,2,105000020 +1686,3,106000060 +1687,3,106000070 +1688,3,106000080 +1689,3,101000070 +1690,4,106000100 +1691,4,106000110 +1692,4,104000090 +1693,1,103000000 +1694,2,103000001 +1695,3,103000002 +1696,4,103000003 +1697,5,103000004 +1698,1,112000000 +1699,2,112000001 +1700,3,112000002 +1701,4,112000003 +1702,5,112000004 +1703,1,120020 +1704,1,120030 +1705,1,120040 +1706,1,120050 +1707,2,120021 +1708,2,120031 +1709,2,120041 +1710,2,120051 +1711,3,120022 +1712,3,120032 +1713,3,120042 +1714,3,120052 +1715,4,120023 +1716,4,120033 +1717,4,120043 +1718,4,120053 +1719,3,120240 +1720,3,120250 +1721,3,120260 +1722,3,120270 +1723,3,120300 +1724,3,120310 +1725,3,120320 +1726,3,120330 +1727,3,120340 +1728,3,120350 +1729,3,120360 +1730,3,120370 +1731,3,120380 +1732,3,120390 +1733,3,120400 +1734,3,120410 +1735,3,120420 +1736,3,120430 +1737,3,120450 +1738,3,120460 +1739,4,140000 +1740,4,150010 +1741,4,150020 +1742,4,150030 +1743,4,150040 +1745,2,103000030 +1746,2,103000040 +1747,2,107000020 +1748,2,101000030 +1749,3,101000050 +1750,3,101000060 +1751,3,101000080 +1752,3,103000060 +1753,3,103000070 +1754,3,103000080 +1755,3,101000040 +1756,4,101000090 +1757,4,102000090 +1758,4,103000100 +1759,4,101000100 +1760,4,101000110 +1761,1,101000000 +1762,2,101000001 +1763,3,101000002 +1764,4,101000008 +1765,5,101000004 +1766,1,120000000 +1767,2,120000001 +1768,3,120000002 +1769,4,120000003 +1770,5,120000004 +1771,3,170003 +1772,4,170004 +1774,2,107000030 +1775,2,107000040 +1776,2,101000020 +1777,3,107000060 +1778,3,107000070 +1779,3,107000080 +1780,3,108000050 +1781,4,107000100 +1782,4,103000090 +1783,1,105000000 +1784,2,105000001 +1785,3,105000002 +1786,4,105000003 +1787,5,105000004 +1788,1,120000000 +1789,2,120000001 +1790,3,120000002 +1791,4,120000003 +1792,5,120000004 +1793,1,130020 +1794,1,130030 +1795,1,130040 +1796,1,130050 +1797,2,130021 +1798,2,130031 +1799,2,130041 +1800,2,130051 +1801,3,130022 +1802,3,130032 +1803,3,130042 +1804,3,130052 +1805,4,130023 +1806,4,130033 +1807,4,130043 +1808,4,130053 +1809,3,130060 +1810,3,130070 +1811,3,130080 +1812,3,130090 +1813,3,130100 +1814,3,130110 +1815,3,130120 +1816,3,130130 +1817,3,130140 +1818,3,130150 +1819,3,130160 +1820,3,130170 +1821,3,130180 +1822,3,130190 +1823,3,130200 +1824,3,130420 +1825,3,130510 +1826,3,130520 +1827,3,130530 +1828,3,130540 +1829,4,140000 +1830,4,150010 +1831,4,150020 +1832,4,150030 +1833,4,150040 +1835,2,102000030 +1836,2,102000040 +1837,2,104000020 +1838,3,102000060 +1839,3,102000070 +1840,3,102000080 +1841,3,103000050 +1842,3,105000050 +1843,4,102000100 +1844,4,102000110 +1845,4,106000090 +1846,1,102000000 +1847,2,102000001 +1848,3,102000002 +1849,4,102000003 +1850,5,102000004 +1851,1,112000000 +1852,2,112000001 +1853,3,112000002 +1854,4,112000003 +1855,5,112000004 +1856,1,120020 +1857,1,120030 +1858,1,120040 +1859,1,120050 +1860,2,120021 +1861,2,120031 +1862,2,120041 +1863,2,120051 +1864,3,120022 +1865,3,120032 +1866,3,120042 +1867,3,120052 +1868,4,120023 +1869,4,120033 +1870,4,120043 +1871,4,120053 +1872,3,120240 +1873,3,120250 +1874,3,120260 +1875,3,120270 +1876,3,120300 +1877,3,120310 +1878,3,120320 +1879,3,120330 +1880,3,120340 +1881,3,120350 +1882,3,120360 +1883,3,120370 +1884,3,120380 +1885,3,120390 +1886,3,120400 +1887,3,120410 +1888,3,120420 +1889,3,120430 +1890,3,120450 +1891,3,120460 +1892,4,140000 +1893,4,150010 +1894,4,150020 +1895,4,150030 +1896,4,150040 +1898,2,108000030 +1899,2,108000040 +1900,2,106000020 +1901,3,108000060 +1902,3,108000070 +1903,3,108000080 +1904,3,107000050 +1905,4,108000100 +1906,4,105000090 +1907,1,108000000 +1908,2,108000001 +1909,3,108000002 +1910,4,108000003 +1911,5,108000004 +1912,1,120000000 +1913,2,120000001 +1914,3,120000002 +1915,4,120000003 +1916,5,120000004 +1917,3,170003 +1918,4,170004 +1920,2,103000030 +1921,2,103000040 +1922,2,107000020 +1923,2,101000030 +1924,3,101000050 +1925,3,101000060 +1926,3,101000080 +1927,3,103000060 +1928,3,103000070 +1929,3,103000080 +1930,3,101000040 +1931,4,101000090 +1932,4,102000090 +1933,4,103000100 +1934,4,101000100 +1935,4,101000110 +1936,1,101000000 +1937,2,101000001 +1938,3,101000002 +1939,4,101000008 +1940,5,101000004 +1941,1,112000000 +1942,2,112000001 +1943,3,112000002 +1944,4,112000003 +1945,5,112000004 +1946,3,170003 +1947,4,170004 +1949,2,105000030 +1950,2,105000040 +1951,2,102000020 +1952,2,103000020 +1953,3,105000060 +1954,3,105000070 +1955,3,105000080 +1956,3,104000050 +1957,3,106000050 +1958,4,105000100 +1959,4,105000110 +1960,4,108000090 +1961,1,109000000 +1962,2,109000001 +1963,3,109000002 +1964,4,109000003 +1965,5,109000004 +1966,1,112000000 +1967,2,112000001 +1968,3,112000002 +1969,4,112000003 +1970,5,112000004 +1971,3,180003 +1972,4,180004 +1974,2,104000030 +1975,2,104000040 +1976,2,108000020 +1977,3,104000060 +1978,3,104000070 +1979,3,104000080 +1980,3,102000050 +1981,4,104000100 +1982,4,104000110 +1983,4,107000090 +1984,1,111000000 +1985,2,111000001 +1986,3,111000002 +1987,4,111000003 +1988,5,111000004 +1989,1,120000000 +1990,2,120000001 +1991,3,120000002 +1992,4,120000003 +1993,5,120000004 +1994,1,110020 +1995,1,110030 +1996,1,110040 +1997,1,110050 +1998,2,110021 +1999,2,110031 +2000,2,110041 +2001,2,110051 +2002,3,110022 +2003,3,110032 +2004,3,110042 +2005,3,110052 +2006,4,110023 +2007,4,110033 +2008,4,110043 +2009,4,110053 +2010,3,110060 +2011,3,110070 +2012,3,110080 +2013,3,110090 +2014,3,110100 +2015,3,110110 +2016,3,110120 +2017,3,110130 +2018,3,110140 +2019,3,110150 +2020,3,110160 +2021,3,110170 +2022,3,110180 +2023,3,110190 +2024,3,110200 +2025,3,110210 +2026,3,110220 +2027,3,110230 +2028,3,110240 +2029,3,110250 +2030,3,110260 +2031,3,110270 +2032,4,140000 +2033,4,150010 +2034,4,150020 +2035,4,150030 +2036,4,150040 +2038,3,105000060 +2039,3,105000070 +2040,3,105000080 +2041,3,104000050 +2042,3,106000050 +2043,4,105000100 +2044,4,105000110 +2045,4,108000090 +2046,5,105000120 +2047,1,109000000 +2048,2,109000001 +2049,3,109000002 +2050,4,109000003 +2051,5,109000004 +2052,1,112000000 +2053,2,112000001 +2054,3,112000002 +2055,4,112000003 +2056,5,112000004 +2057,3,170004 +2059,3,101000050 +2060,3,101000060 +2061,3,101000080 +2062,3,103000060 +2063,3,103000070 +2064,3,103000080 +2065,3,101000040 +2066,4,101000090 +2067,4,102000090 +2068,4,103000100 +2069,4,101000100 +2070,4,101000110 +2071,5,101000120 +2072,5,103000120 +2073,1,101000000 +2074,2,101000001 +2075,3,101000002 +2076,4,101000008 +2077,5,101000004 +2078,1,120000000 +2079,2,120000001 +2080,3,120000002 +2081,4,120000003 +2082,5,120000004 +2083,3,170004 +2085,3,107000060 +2086,3,107000070 +2087,3,107000080 +2088,3,108000050 +2089,4,107000100 +2090,4,103000090 +2091,5,107000110 +2092,1,105000000 +2093,2,105000001 +2094,3,105000002 +2095,4,105000003 +2096,5,105000004 +2097,1,120000000 +2098,2,120000001 +2099,3,120000002 +2100,4,120000003 +2101,5,120000004 +2102,3,170004 +2104,3,101000050 +2105,3,101000060 +2106,3,101000080 +2107,3,103000060 +2108,3,103000070 +2109,3,103000080 +2110,3,101000040 +2111,4,101000090 +2112,4,102000090 +2113,4,103000100 +2114,4,101000100 +2115,4,101000110 +2116,5,101000120 +2117,5,103000120 +2118,1,101000000 +2119,2,101000001 +2120,3,101000002 +2121,4,101000008 +2122,5,101000004 +2123,1,112000000 +2124,2,112000001 +2125,3,112000002 +2126,4,112000003 +2127,5,112000004 +2128,1,130020 +2129,1,130030 +2130,1,130040 +2131,1,130050 +2132,2,130021 +2133,2,130031 +2134,2,130041 +2135,2,130051 +2136,3,130022 +2137,3,130032 +2138,3,130042 +2139,3,130052 +2140,4,130023 +2141,4,130033 +2142,4,130043 +2143,4,130053 +2144,5,130024 +2145,5,130034 +2146,5,130044 +2147,5,130054 +2148,3,130060 +2149,3,130070 +2150,3,130080 +2151,3,130090 +2152,3,130100 +2153,3,130110 +2154,3,130120 +2155,3,130130 +2156,3,130140 +2157,3,130150 +2158,3,130160 +2159,3,130170 +2160,3,130180 +2161,3,130190 +2162,3,130200 +2163,3,130420 +2164,3,130510 +2165,3,130520 +2166,3,130530 +2167,3,130540 +2168,4,140000 +2169,4,150010 +2170,4,150020 +2171,4,150030 +2172,4,150040 +2174,3,102000060 +2175,3,102000070 +2176,3,102000080 +2177,3,103000050 +2178,3,105000050 +2179,4,102000100 +2180,4,102000110 +2181,4,106000090 +2182,5,102000120 +2183,1,102000000 +2184,2,102000001 +2185,3,102000002 +2186,4,102000003 +2187,5,102000004 +2188,1,112000000 +2189,2,112000001 +2190,3,112000002 +2191,4,112000003 +2192,5,112000004 +2193,3,170004 +2195,3,101000050 +2196,3,101000060 +2197,3,101000080 +2198,3,103000060 +2199,3,103000070 +2200,3,103000080 +2201,3,101000040 +2202,4,101000090 +2203,4,102000090 +2204,4,103000100 +2205,4,101000100 +2206,4,101000110 +2207,5,101000120 +2208,5,103000120 +2209,1,101000000 +2210,2,101000001 +2211,3,101000002 +2212,4,101000008 +2213,5,101000004 +2214,1,120000000 +2215,2,120000001 +2216,3,120000002 +2217,4,120000003 +2218,5,120000004 +2219,3,170004 +2221,3,106000060 +2222,3,106000070 +2223,3,106000080 +2224,3,101000070 +2225,4,106000100 +2226,4,106000110 +2227,4,104000090 +2228,5,106000120 +2229,1,103000000 +2230,2,103000001 +2231,3,103000002 +2232,4,103000003 +2233,5,103000004 +2234,1,112000000 +2235,2,112000001 +2236,3,112000002 +2237,4,112000003 +2238,5,112000004 +2239,1,130020 +2240,1,130030 +2241,1,130040 +2242,1,130050 +2243,2,130021 +2244,2,130031 +2245,2,130041 +2246,2,130051 +2247,3,130022 +2248,3,130032 +2249,3,130042 +2250,3,130052 +2251,4,130023 +2252,4,130033 +2253,4,130043 +2254,4,130053 +2255,5,130024 +2256,5,130034 +2257,5,130044 +2258,5,130054 +2259,3,130060 +2260,3,130070 +2261,3,130080 +2262,3,130090 +2263,3,130100 +2264,3,130110 +2265,3,130120 +2266,3,130130 +2267,3,130140 +2268,3,130150 +2269,3,130160 +2270,3,130170 +2271,3,130180 +2272,3,130190 +2273,3,130200 +2274,3,130420 +2275,3,130510 +2276,3,130520 +2277,3,130530 +2278,3,130540 +2279,4,140000 +2280,4,150010 +2281,4,150020 +2282,4,150030 +2283,4,150040 +2285,3,108000060 +2286,3,108000070 +2287,3,108000080 +2288,3,107000050 +2289,4,108000100 +2290,4,105000090 +2291,5,108000110 +2292,1,108000000 +2293,2,108000001 +2294,3,108000002 +2295,4,108000003 +2296,5,108000004 +2297,1,120000000 +2298,2,120000001 +2299,3,120000002 +2300,4,120000003 +2301,5,120000004 +2302,1,120020 +2303,1,120030 +2304,1,120040 +2305,1,120050 +2306,2,120021 +2307,2,120031 +2308,2,120041 +2309,2,120051 +2310,3,120022 +2311,3,120032 +2312,3,120042 +2313,3,120052 +2314,4,120023 +2315,4,120033 +2316,4,120043 +2317,4,120053 +2318,5,120024 +2319,5,120034 +2320,5,120044 +2321,5,120054 +2322,3,120240 +2323,3,120250 +2324,3,120260 +2325,3,120270 +2326,3,120300 +2327,3,120310 +2328,3,120320 +2329,3,120330 +2330,3,120340 +2331,3,120350 +2332,3,120360 +2333,3,120370 +2334,3,120380 +2335,3,120390 +2336,3,120400 +2337,3,120410 +2338,3,120420 +2339,3,120430 +2340,3,120450 +2341,3,120460 +2342,4,140000 +2343,4,150010 +2344,4,150020 +2345,4,150030 +2346,4,150040 +2348,3,104000060 +2349,3,104000070 +2350,3,104000080 +2351,3,102000050 +2352,4,104000100 +2353,4,104000110 +2354,4,107000090 +2355,5,104000120 +2356,1,111000000 +2357,2,111000001 +2358,3,111000002 +2359,4,111000003 +2360,5,111000004 +2361,1,120000000 +2362,2,120000001 +2363,3,120000002 +2364,4,120000003 +2365,5,120000004 +2366,1,110020 +2367,1,110030 +2368,1,110040 +2369,1,110050 +2370,2,110021 +2371,2,110031 +2372,2,110041 +2373,2,110051 +2374,3,110022 +2375,3,110032 +2376,3,110042 +2377,3,110052 +2378,4,110023 +2379,4,110033 +2380,4,110043 +2381,4,110053 +2382,5,110024 +2383,5,110034 +2384,5,110044 +2385,5,110054 +2386,3,110060 +2387,3,110070 +2388,3,110080 +2389,3,110090 +2390,3,110100 +2391,3,110110 +2392,3,110120 +2393,3,110130 +2394,3,110140 +2395,3,110150 +2396,3,110160 +2397,3,110170 +2398,3,110180 +2399,3,110190 +2400,3,110200 +2401,3,110210 +2402,3,110220 +2403,3,110230 +2404,3,110240 +2405,3,110250 +2406,3,110260 +2407,3,110270 +2408,4,140000 +2409,4,150010 +2410,4,150020 +2411,4,150030 +2412,4,150040 +2414,3,105000060 +2415,3,105000070 +2416,3,105000080 +2417,3,104000050 +2418,3,106000050 +2419,4,105000100 +2420,4,105000110 +2421,4,108000090 +2422,5,105000120 +2423,1,109000000 +2424,2,109000001 +2425,3,109000002 +2426,4,109000003 +2427,5,109000004 +2428,1,112000000 +2429,2,112000001 +2430,3,112000002 +2431,4,112000003 +2432,5,112000004 +2433,3,170004 +2435,3,104000060 +2436,3,104000070 +2437,3,104000080 +2438,3,102000050 +2439,4,104000100 +2440,4,104000110 +2441,4,107000090 +2442,5,104000120 +2443,1,111000000 +2444,2,111000001 +2445,3,111000002 +2446,4,111000003 +2447,5,111000004 +2448,1,120000000 +2449,2,120000001 +2450,3,120000002 +2451,4,120000003 +2452,5,120000004 +2453,1,130020 +2454,1,130030 +2455,1,130040 +2456,1,130050 +2457,2,130021 +2458,2,130031 +2459,2,130041 +2460,2,130051 +2461,3,130022 +2462,3,130032 +2463,3,130042 +2464,3,130052 +2465,4,130023 +2466,4,130033 +2467,4,130043 +2468,4,130053 +2469,5,130024 +2470,5,130034 +2471,5,130044 +2472,5,130054 +2473,3,130060 +2474,3,130070 +2475,3,130080 +2476,3,130090 +2477,3,130100 +2478,3,130110 +2479,3,130120 +2480,3,130130 +2481,3,130140 +2482,3,130150 +2483,3,130160 +2484,3,130170 +2485,3,130180 +2486,3,130190 +2487,3,130200 +2488,3,130420 +2489,3,130510 +2490,3,130520 +2491,3,130530 +2492,3,130540 +2493,4,140000 +2494,4,150010 +2495,4,150020 +2496,4,150030 +2497,4,150040 +2500,1,101000000 +2501,2,101000001 +2502,3,101000002 +2503,4,101000008 +2504,5,101000004 +2505,1,102000000 +2506,2,102000001 +2507,3,102000002 +2508,4,102000003 +2509,5,102000004 +2510,1,103000000 +2511,2,103000001 +2512,3,103000002 +2513,4,103000003 +2514,5,103000004 +2515,1,105000000 +2516,2,105000001 +2517,3,105000002 +2518,4,105000003 +2519,5,105000004 +2520,1,108000000 +2521,2,108000001 +2522,3,108000002 +2523,4,108000003 +2524,5,108000004 +2525,1,109000000 +2526,2,109000001 +2527,3,109000002 +2528,4,109000003 +2529,5,109000004 +2530,1,111000000 +2531,2,111000001 +2532,3,111000002 +2533,4,111000003 +2534,5,111000004 +2535,1,112000000 +2536,2,112000001 +2537,3,112000002 +2538,4,112000003 +2539,5,112000004 +2540,1,120000000 +2541,2,120000001 +2542,3,120000002 +2543,4,120000003 +2544,5,120000004 +2545,1,101000010 +2546,2,101000020 +2547,2,101000030 +2548,3,101000040 +2549,3,101000050 +2550,3,101000060 +2551,3,101000070 +2552,3,101000080 +2553,1,102000010 +2554,2,102000020 +2555,2,102000030 +2556,2,102000040 +2557,3,102000050 +2558,3,102000060 +2559,3,102000070 +2560,3,102000080 +2561,1,103000010 +2562,2,103000020 +2563,2,103000030 +2564,2,103000040 +2565,3,103000050 +2566,3,103000060 +2567,3,103000070 +2568,3,103000080 +2569,1,104000010 +2570,2,104000020 +2571,2,104000030 +2572,2,104000040 +2573,3,104000050 +2574,3,104000060 +2575,3,104000070 +2576,3,104000080 +2577,1,105000010 +2578,2,105000020 +2579,2,105000030 +2580,2,105000040 +2581,3,105000050 +2582,3,105000060 +2583,3,105000070 +2584,3,105000080 +2585,1,106000010 +2586,2,106000020 +2587,2,106000030 +2588,2,106000040 +2589,3,106000050 +2590,3,106000060 +2591,3,106000070 +2592,3,106000080 +2593,1,107000010 +2594,2,107000020 +2595,2,107000030 +2596,2,107000040 +2597,3,107000050 +2598,3,107000060 +2599,3,107000070 +2600,3,107000080 +2601,1,108000010 +2602,2,108000020 +2603,2,108000030 +2604,2,108000040 +2605,3,108000050 +2606,3,108000060 +2607,3,108000070 +2608,3,108000080 +2609,2,180001 +2611,1,101000000 +2612,2,101000001 +2613,3,101000002 +2614,4,101000008 +2615,5,101000004 +2616,1,102000000 +2617,2,102000001 +2618,3,102000002 +2619,4,102000003 +2620,5,102000004 +2621,1,103000000 +2622,2,103000001 +2623,3,103000002 +2624,4,103000003 +2625,5,103000004 +2626,1,105000000 +2627,2,105000001 +2628,3,105000002 +2629,4,105000003 +2630,5,105000004 +2631,1,108000000 +2632,2,108000001 +2633,3,108000002 +2634,4,108000003 +2635,5,108000004 +2636,1,109000000 +2637,2,109000001 +2638,3,109000002 +2639,4,109000003 +2640,5,109000004 +2641,1,111000000 +2642,2,111000001 +2643,3,111000002 +2644,4,111000003 +2645,5,111000004 +2646,1,112000000 +2647,2,112000001 +2648,3,112000002 +2649,4,112000003 +2650,5,112000004 +2651,1,120000000 +2652,2,120000001 +2653,3,120000002 +2654,4,120000003 +2655,5,120000004 +2656,1,101000010 +2657,2,101000020 +2658,2,101000030 +2659,3,101000040 +2660,3,101000050 +2661,3,101000060 +2662,3,101000070 +2663,3,101000080 +2664,1,102000010 +2665,2,102000020 +2666,2,102000030 +2667,2,102000040 +2668,3,102000050 +2669,3,102000060 +2670,3,102000070 +2671,3,102000080 +2672,1,103000010 +2673,2,103000020 +2674,2,103000030 +2675,2,103000040 +2676,3,103000050 +2677,3,103000060 +2678,3,103000070 +2679,3,103000080 +2680,1,104000010 +2681,2,104000020 +2682,2,104000030 +2683,2,104000040 +2684,3,104000050 +2685,3,104000060 +2686,3,104000070 +2687,3,104000080 +2688,1,105000010 +2689,2,105000020 +2690,2,105000030 +2691,2,105000040 +2692,3,105000050 +2693,3,105000060 +2694,3,105000070 +2695,3,105000080 +2696,1,106000010 +2697,2,106000020 +2698,2,106000030 +2699,2,106000040 +2700,3,106000050 +2701,3,106000060 +2702,3,106000070 +2703,3,106000080 +2704,1,107000010 +2705,2,107000020 +2706,2,107000030 +2707,2,107000040 +2708,3,107000050 +2709,3,107000060 +2710,3,107000070 +2711,3,107000080 +2712,1,108000010 +2713,2,108000020 +2714,2,108000030 +2715,2,108000040 +2716,3,108000050 +2717,3,108000060 +2718,3,108000070 +2719,3,108000080 +2720,1,109000010 +2721,2,109000020 +2722,2,109000030 +2723,2,109000040 +2724,3,109000050 +2725,3,109000060 +2726,3,109000070 +2727,3,109000080 +2728,2,180001 +2731,1,101000000 +2732,2,101000001 +2733,3,101000002 +2734,4,101000008 +2735,5,101000004 +2736,1,102000000 +2737,2,102000001 +2738,3,102000002 +2739,4,102000003 +2740,5,102000004 +2741,1,103000000 +2742,2,103000001 +2743,3,103000002 +2744,4,103000003 +2745,5,103000004 +2746,1,105000000 +2747,2,105000001 +2748,3,105000002 +2749,4,105000003 +2750,5,105000004 +2751,1,107000000 +2752,2,107000001 +2753,3,107000002 +2754,4,107000003 +2755,5,107000004 +2756,1,108000000 +2757,2,108000001 +2758,3,108000002 +2759,4,108000003 +2760,5,108000004 +2761,1,109000000 +2762,2,109000001 +2763,3,109000002 +2764,4,109000003 +2765,5,109000004 +2766,1,111000000 +2767,2,111000001 +2768,3,111000002 +2769,4,111000003 +2770,5,111000004 +2771,1,112000000 +2772,2,112000001 +2773,3,112000002 +2774,4,112000003 +2775,5,112000004 +2776,1,120000000 +2777,2,120000001 +2778,3,120000002 +2779,4,120000003 +2780,5,120000004 +2781,1,101000010 +2782,2,101000020 +2783,2,101000030 +2784,3,101000040 +2785,3,101000050 +2786,3,101000060 +2787,3,101000070 +2788,3,101000080 +2789,1,102000010 +2790,2,102000020 +2791,2,102000030 +2792,2,102000040 +2793,3,102000050 +2794,3,102000060 +2795,3,102000070 +2796,3,102000080 +2797,1,103000010 +2798,2,103000020 +2799,2,103000030 +2800,2,103000040 +2801,3,103000050 +2802,3,103000060 +2803,3,103000070 +2804,3,103000080 +2805,1,104000010 +2806,2,104000020 +2807,2,104000030 +2808,2,104000040 +2809,3,104000050 +2810,3,104000060 +2811,3,104000070 +2812,3,104000080 +2813,1,105000010 +2814,2,105000020 +2815,2,105000030 +2816,2,105000040 +2817,3,105000050 +2818,3,105000060 +2819,3,105000070 +2820,3,105000080 +2821,1,106000010 +2822,2,106000020 +2823,2,106000030 +2824,2,106000040 +2825,3,106000050 +2826,3,106000060 +2827,3,106000070 +2828,3,106000080 +2829,1,107000010 +2830,2,107000020 +2831,2,107000030 +2832,2,107000040 +2833,3,107000050 +2834,3,107000060 +2835,3,107000070 +2836,3,107000080 +2837,1,108000010 +2838,2,108000020 +2839,2,108000030 +2840,2,108000040 +2841,3,108000050 +2842,3,108000060 +2843,3,108000070 +2844,3,108000080 +2845,1,109000010 +2846,2,109000020 +2847,2,109000030 +2848,2,109000040 +2849,3,109000050 +2850,3,109000060 +2851,3,109000070 +2852,3,109000080 +2853,1,110000010 +2854,2,110000020 +2855,2,110000030 +2856,2,110000040 +2857,3,110000050 +2858,3,110000060 +2859,3,110000070 +2860,3,110000080 +2861,2,180001 +2863,1,107000000 +2864,2,107000001 +2865,3,107000002 +2866,4,107000003 +2867,5,107000004 +2868,1,120000000 +2869,2,120000001 +2870,3,120000002 +2871,4,120000003 +2872,5,120000004 +2874,3,110000070 +2875,3,110000080 +2876,4,110000100 +2877,5,110000110 +2878,1,107000000 +2879,2,107000001 +2880,3,107000002 +2881,4,107000003 +2882,5,107000004 +2883,1,120000000 +2884,2,120000001 +2885,3,120000002 +2886,4,120000003 +2887,5,120000004 +2888,3,120023 +2889,3,120033 +2890,3,120043 +2891,3,120053 +2892,4,120024 +2893,4,120034 +2894,4,120044 +2895,4,120054 +2896,3,120240 +2897,3,120250 +2898,3,120260 +2899,3,120270 +2900,3,120300 +2901,3,120310 +2902,3,120320 +2903,3,120330 +2904,3,120340 +2905,3,120350 +2906,3,120360 +2907,3,120370 +2908,3,120380 +2909,3,120390 +2910,3,120400 +2911,3,120410 +2912,3,120420 +2913,3,120430 +2914,3,120450 +2915,3,120460 +2916,3,120550 +2917,3,120560 +2918,3,120570 +2919,3,120990 +2920,3,121000 +2921,3,121010 +2922,3,121020 +2923,4,140000 +2924,4,150010 +2925,4,150020 +2926,4,150030 +2927,4,150040 +2929,3,108000060 +2930,3,108000070 +2931,3,108000080 +2932,3,107000050 +2933,4,108000100 +2934,4,105000090 +2935,5,108000110 +2936,1,108000000 +2937,2,108000001 +2938,3,108000002 +2939,4,108000003 +2940,5,108000004 +2941,1,120000000 +2942,2,120000001 +2943,3,120000002 +2944,4,120000003 +2945,5,120000004 +2946,3,170004 +2948,3,102000060 +2949,3,102000070 +2950,3,102000080 +2951,3,103000050 +2952,3,105000050 +2953,4,102000100 +2954,4,102000110 +2955,4,106000090 +2956,4,109000090 +2957,5,102000120 +2958,1,102000000 +2959,2,102000001 +2960,3,102000002 +2961,4,102000003 +2962,5,102000004 +2963,1,112000000 +2964,2,112000001 +2965,3,112000002 +2966,4,112000003 +2967,5,112000004 +2968,3,170004 +2970,3,101000050 +2971,3,101000060 +2972,3,101000080 +2973,3,103000060 +2974,3,103000070 +2975,3,103000080 +2976,3,101000040 +2977,3,109000060 +2978,3,109000070 +2979,3,109000080 +2980,3,110000050 +2981,4,101000090 +2982,4,102000090 +2983,4,103000100 +2984,4,101000100 +2985,4,101000110 +2986,4,109000100 +2987,5,101000120 +2988,5,103000120 +2989,5,109000110 +2990,1,101000000 +2991,2,101000001 +2992,3,101000002 +2993,4,101000008 +2994,5,101000004 +2995,1,112000000 +2996,2,112000001 +2997,3,112000002 +2998,4,112000003 +2999,5,112000004 +3000,3,120023 +3001,3,120033 +3002,3,120043 +3003,3,120053 +3004,4,120024 +3005,4,120034 +3006,4,120044 +3007,4,120054 +3008,3,120240 +3009,3,120250 +3010,3,120260 +3011,3,120270 +3012,3,120300 +3013,3,120310 +3014,3,120320 +3015,3,120330 +3016,3,120340 +3017,3,120350 +3018,3,120360 +3019,3,120370 +3020,3,120380 +3021,3,120390 +3022,3,120400 +3023,3,120410 +3024,3,120420 +3025,3,120430 +3026,3,120450 +3027,3,120460 +3028,3,120550 +3029,3,120560 +3030,3,120570 +3031,3,120990 +3032,3,121000 +3033,3,121010 +3034,3,121020 +3035,4,140000 +3036,4,150010 +3037,4,150020 +3038,4,150030 +3039,4,150040 +3041,3,105000060 +3042,3,105000070 +3043,3,105000080 +3044,3,104000050 +3045,3,106000050 +3046,4,105000100 +3047,4,105000110 +3048,4,108000090 +3049,4,110000090 +3050,5,105000120 +3051,1,109000000 +3052,2,109000001 +3053,3,109000002 +3054,4,109000003 +3055,5,109000004 +3056,1,112000000 +3057,2,112000001 +3058,3,112000002 +3059,4,112000003 +3060,5,112000004 +3061,3,170004 +3063,3,107000060 +3064,3,107000070 +3065,3,107000080 +3066,3,108000050 +3067,3,109000050 +3068,4,107000100 +3069,4,103000090 +3070,5,107000110 +3071,1,105000000 +3072,2,105000001 +3073,3,105000002 +3074,4,105000003 +3075,5,105000004 +3076,1,120000000 +3077,2,120000001 +3078,3,120000002 +3079,4,120000003 +3080,5,120000004 +3081,3,130023 +3082,3,130033 +3083,3,130043 +3084,3,130053 +3085,4,130024 +3086,4,130034 +3087,4,130044 +3088,4,130054 +3089,3,130060 +3090,3,130070 +3091,3,130080 +3092,3,130090 +3093,3,130100 +3094,3,130110 +3095,3,130120 +3096,3,130130 +3097,3,130140 +3098,3,130150 +3099,3,130160 +3100,3,130170 +3101,3,130180 +3102,3,130190 +3103,3,130200 +3104,3,130420 +3105,3,130510 +3106,3,130520 +3107,3,130530 +3108,3,130540 +3109,3,130660 +3110,4,140000 +3111,4,150010 +3112,4,150020 +3113,4,150030 +3114,4,150040 +3116,3,106000060 +3117,3,106000070 +3118,3,106000080 +3119,3,101000070 +3120,3,110000060 +3121,4,106000100 +3122,4,106000110 +3123,4,104000090 +3124,5,106000120 +3125,1,103000000 +3126,2,103000001 +3127,3,103000002 +3128,4,103000003 +3129,5,103000004 +3130,1,112000000 +3131,2,112000001 +3132,3,112000002 +3133,4,112000003 +3134,5,112000004 +3135,3,170004 +3137,3,104000060 +3138,3,104000070 +3139,3,104000080 +3140,3,102000050 +3141,4,104000100 +3142,4,104000110 +3143,4,107000090 +3144,5,104000120 +3145,1,111000000 +3146,2,111000001 +3147,3,111000002 +3148,4,111000003 +3149,5,111000004 +3150,1,120000000 +3151,2,120000001 +3152,3,120000002 +3153,4,120000003 +3154,5,120000004 +3155,3,110023 +3156,3,110033 +3157,3,110043 +3158,3,110053 +3159,4,110024 +3160,4,110034 +3161,4,110044 +3162,4,110054 +3163,3,110060 +3164,3,110070 +3165,3,110080 +3166,3,110090 +3167,3,110100 +3168,3,110110 +3169,3,110120 +3170,3,110130 +3171,3,110140 +3172,3,110150 +3173,3,110160 +3174,3,110170 +3175,3,110180 +3176,3,110190 +3177,3,110200 +3178,3,110210 +3179,3,110220 +3180,3,110230 +3181,3,110240 +3182,3,110250 +3183,3,110260 +3184,3,110270 +3185,3,110620 +3186,3,110670 +3187,4,140000 +3188,4,150010 +3189,4,150020 +3190,4,150030 +3191,4,150040 +3193,3,101000050 +3194,3,101000060 +3195,3,101000080 +3196,3,103000060 +3197,3,103000070 +3198,3,103000080 +3199,3,101000040 +3200,3,109000060 +3201,3,109000070 +3202,3,109000080 +3203,3,110000050 +3204,4,101000090 +3205,4,102000090 +3206,4,103000100 +3207,4,101000100 +3208,4,101000110 +3209,4,109000100 +3210,5,101000120 +3211,5,103000120 +3212,5,109000110 +3213,1,101000000 +3214,2,101000001 +3215,3,101000002 +3216,4,101000008 +3217,5,101000004 +3218,1,120000000 +3219,2,120000001 +3220,3,120000002 +3221,4,120000003 +3222,5,120000004 +3223,3,170004 +3225,3,110000070 +3226,3,110000080 +3227,4,110000100 +3228,5,110000110 +3229,1,107000000 +3230,2,107000001 +3231,3,107000002 +3232,4,107000003 +3233,5,107000004 +3234,1,120000000 +3235,2,120000001 +3236,3,120000002 +3237,4,120000003 +3238,5,120000004 +3239,3,180004 +3241,3,105000060 +3242,3,105000070 +3243,3,105000080 +3244,3,104000050 +3245,3,106000050 +3246,4,105000100 +3247,4,105000110 +3248,4,108000090 +3249,4,110000090 +3250,5,105000120 +3251,1,109000000 +3252,2,109000001 +3253,3,109000002 +3254,4,109000003 +3255,5,109000004 +3256,1,112000000 +3257,2,112000001 +3258,3,112000002 +3259,4,112000003 +3260,5,112000004 +3261,3,170004 +3263,3,108000060 +3264,3,108000070 +3265,3,108000080 +3266,3,107000050 +3267,4,108000100 +3268,4,105000090 +3269,5,108000110 +3270,1,108000000 +3271,2,108000001 +3272,3,108000002 +3273,4,108000003 +3274,5,108000004 +3275,1,120000000 +3276,2,120000001 +3277,3,120000002 +3278,4,120000003 +3279,5,120000004 +3280,4,120024 +3281,4,120034 +3282,4,120044 +3283,4,120054 +3284,3,120240 +3285,3,120250 +3286,3,120260 +3287,3,120270 +3288,3,120300 +3289,3,120310 +3290,3,120320 +3291,3,120330 +3292,3,120340 +3293,3,120350 +3294,3,120360 +3295,3,120370 +3296,3,120380 +3297,3,120390 +3298,3,120400 +3299,3,120410 +3300,3,120420 +3301,3,120430 +3302,3,120450 +3303,3,120460 +3304,3,120550 +3305,3,120560 +3306,3,120570 +3307,3,120990 +3308,3,121000 +3309,3,121010 +3310,3,121020 +3311,4,140000 +3312,4,150010 +3313,4,150020 +3314,4,150030 +3315,4,150040 +3317,3,104000060 +3318,3,104000070 +3319,3,104000080 +3320,3,102000050 +3321,4,104000100 +3322,4,104000110 +3323,4,107000090 +3324,5,104000120 +3325,1,111000000 +3326,2,111000001 +3327,3,111000002 +3328,4,111000003 +3329,5,111000004 +3330,1,120000000 +3331,2,120000001 +3332,3,120000002 +3333,4,120000003 +3334,5,120000004 +3335,4,110024 +3336,4,110034 +3337,4,110044 +3338,4,110054 +3339,3,110060 +3340,3,110070 +3341,3,110080 +3342,3,110090 +3343,3,110100 +3344,3,110110 +3345,3,110120 +3346,3,110130 +3347,3,110140 +3348,3,110150 +3349,3,110160 +3350,3,110170 +3351,3,110180 +3352,3,110190 +3353,3,110200 +3354,3,110210 +3355,3,110220 +3356,3,110230 +3357,3,110240 +3358,3,110250 +3359,3,110260 +3360,3,110270 +3361,3,110620 +3362,3,110670 +3363,4,140000 +3364,4,150010 +3365,4,150020 +3366,4,150030 +3367,4,150040 +3369,3,101000050 +3370,3,101000060 +3371,3,101000080 +3372,3,103000060 +3373,3,103000070 +3374,3,103000080 +3375,3,101000040 +3376,3,109000060 +3377,3,109000070 +3378,3,109000080 +3379,3,110000050 +3380,4,101000090 +3381,4,102000090 +3382,4,103000100 +3383,4,101000100 +3384,4,101000110 +3385,4,109000100 +3386,5,101000120 +3387,5,103000120 +3388,5,109000110 +3389,1,101000000 +3390,2,101000001 +3391,3,101000002 +3392,4,101000008 +3393,5,101000004 +3394,1,112000000 +3395,2,112000001 +3396,3,112000002 +3397,4,112000003 +3398,5,112000004 +3399,3,170004 +3401,3,107000060 +3402,3,107000070 +3403,3,107000080 +3404,3,108000050 +3405,3,109000050 +3406,4,107000100 +3407,4,103000090 +3408,5,107000110 +3409,1,105000000 +3410,2,105000001 +3411,3,105000002 +3412,4,105000003 +3413,5,105000004 +3414,1,120000000 +3415,2,120000001 +3416,3,120000002 +3417,4,120000003 +3418,5,120000004 +3419,4,120024 +3420,4,120034 +3421,4,120044 +3422,4,120054 +3423,3,120240 +3424,3,120250 +3425,3,120260 +3426,3,120270 +3427,3,120300 +3428,3,120310 +3429,3,120320 +3430,3,120330 +3431,3,120340 +3432,3,120350 +3433,3,120360 +3434,3,120370 +3435,3,120380 +3436,3,120390 +3437,3,120400 +3438,3,120410 +3439,3,120420 +3440,3,120430 +3441,3,120450 +3442,3,120460 +3443,3,120550 +3444,3,120560 +3445,3,120570 +3446,3,120990 +3447,3,121000 +3448,3,121010 +3449,3,121020 +3450,4,140000 +3451,4,150010 +3452,4,150020 +3453,4,150030 +3454,4,150040 +3456,3,108000060 +3457,3,108000070 +3458,3,108000080 +3459,3,107000050 +3460,4,108000100 +3461,4,105000090 +3462,5,108000110 +3463,1,108000000 +3464,2,108000001 +3465,3,108000002 +3466,4,108000003 +3467,5,108000004 +3468,1,120000000 +3469,2,120000001 +3470,3,120000002 +3471,4,120000003 +3472,5,120000004 +3473,3,170004 +3475,3,102000060 +3476,3,102000070 +3477,3,102000080 +3478,3,103000050 +3479,3,105000050 +3480,4,102000100 +3481,4,102000110 +3482,4,106000090 +3483,4,109000090 +3484,5,102000120 +3485,1,102000000 +3486,2,102000001 +3487,3,102000002 +3488,4,102000003 +3489,5,102000004 +3490,1,112000000 +3491,2,112000001 +3492,3,112000002 +3493,4,112000003 +3494,5,112000004 +3495,3,180004 +3497,3,106000060 +3498,3,106000070 +3499,3,106000080 +3500,3,101000070 +3501,3,110000060 +3502,4,106000100 +3503,4,106000110 +3504,4,104000090 +3505,5,106000120 +3506,1,103000000 +3507,2,103000001 +3508,3,103000002 +3509,4,103000003 +3510,5,103000004 +3511,1,112000000 +3512,2,112000001 +3513,3,112000002 +3514,4,112000003 +3515,5,112000004 +3516,4,130024 +3517,4,130034 +3518,4,130044 +3519,4,130054 +3520,3,130060 +3521,3,130070 +3522,3,130080 +3523,3,130090 +3524,3,130100 +3525,3,130110 +3526,3,130120 +3527,3,130130 +3528,3,130140 +3529,3,130150 +3530,3,130160 +3531,3,130170 +3532,3,130180 +3533,3,130190 +3534,3,130200 +3535,3,130420 +3536,3,130510 +3537,3,130520 +3538,3,130530 +3539,3,130540 +3540,3,130660 +3541,4,140000 +3542,4,150010 +3543,4,150020 +3544,4,150030 +3545,4,150040 +3547,3,110000070 +3548,3,110000080 +3549,4,110000100 +3550,5,110000110 +3551,1,107000000 +3552,2,107000001 +3553,3,107000002 +3554,4,107000003 +3555,5,107000004 +3556,1,120000000 +3557,2,120000001 +3558,3,120000002 +3559,4,120000003 +3560,5,120000004 +3561,3,170004 +3563,3,105000060 +3564,3,105000070 +3565,3,105000080 +3566,3,104000050 +3567,3,106000050 +3568,4,105000100 +3569,4,105000110 +3570,4,108000090 +3571,4,110000090 +3572,5,105000120 +3573,1,109000000 +3574,2,109000001 +3575,3,109000002 +3576,4,109000003 +3577,5,109000004 +3578,1,112000000 +3579,2,112000001 +3580,3,112000002 +3581,4,112000003 +3582,5,112000004 +3583,4,120024 +3584,4,120034 +3585,4,120044 +3586,4,120054 +3587,3,120240 +3588,3,120250 +3589,3,120260 +3590,3,120270 +3591,3,120300 +3592,3,120310 +3593,3,120320 +3594,3,120330 +3595,3,120340 +3596,3,120350 +3597,3,120360 +3598,3,120370 +3599,3,120380 +3600,3,120390 +3601,3,120400 +3602,3,120410 +3603,3,120420 +3604,3,120430 +3605,3,120450 +3606,3,120460 +3607,3,120550 +3608,3,120560 +3609,3,120570 +3610,3,120990 +3611,3,121000 +3612,3,121010 +3613,3,121020 +3614,4,140000 +3615,4,150010 +3616,4,150020 +3617,4,150030 +3618,4,150040 +3620,1,170000 +3621,2,170001 +3622,3,170002 +3623,4,170003 +3624,5,170004 +3625,1,180000 +3626,2,180001 +3627,3,180002 +3628,4,180003 +3629,5,180004 +3630,4,140000 +3631,1,201000010 +3632,1,292000010 +3633,1,299000040 +3634,1,299000070 +3635,1,299000110 +3636,1,299000120 +3637,1,299000140 +3638,2,202000010 +3639,2,290000010 +3640,2,299000010 +3641,2,299000150 +3642,2,299000190 +3643,2,299000200 +3644,2,299000210 +3645,3,298000050 +3646,3,298000060 +3647,3,299000060 +3648,3,299000170 +3649,5,150010 +3650,5,150020 +3651,5,150030 +3652,5,150040 +3654,3,105000060 +3655,3,105000070 +3656,3,105000080 +3657,3,104000050 +3658,3,106000050 +3659,4,105000100 +3660,4,105000110 +3661,4,108000090 +3662,4,110000090 +3663,5,105000120 +3664,1,109000000 +3665,2,109000001 +3666,3,109000002 +3667,4,109000003 +3668,5,109000004 +3669,1,112000000 +3670,2,112000001 +3671,3,112000002 +3672,4,112000003 +3673,5,112000004 +3674,3,170004 +3676,3,108000060 +3677,3,108000070 +3678,3,108000080 +3679,3,107000050 +3680,4,108000100 +3681,4,105000090 +3682,5,108000110 +3683,1,108000000 +3684,2,108000001 +3685,3,108000002 +3686,4,108000003 +3687,5,108000004 +3688,1,120000000 +3689,2,120000001 +3690,3,120000002 +3691,4,120000003 +3692,5,120000004 +3693,3,180004 +3695,3,106000060 +3696,3,106000070 +3697,3,106000080 +3698,3,101000070 +3699,3,110000060 +3700,4,106000100 +3701,4,106000110 +3702,4,104000090 +3703,5,106000120 +3704,1,103000000 +3705,2,103000001 +3706,3,103000002 +3707,4,103000003 +3708,5,103000004 +3709,1,112000000 +3710,2,112000001 +3711,3,112000002 +3712,4,112000003 +3713,5,112000004 +3714,3,170004 +3716,3,104000170 +3717,1,115000000 +3718,2,115000001 +3719,3,115000002 +3720,4,115000003 +3721,5,115000004 +3722,1,120000000 +3723,2,120000001 +3724,3,120000002 +3725,4,120000003 +3726,5,120000004 +3727,4,120024 +3728,4,120034 +3729,4,120044 +3730,4,120054 +3731,3,120241 +3732,3,120251 +3733,3,120261 +3734,3,120271 +3735,3,120300 +3736,3,120310 +3737,3,120320 +3738,3,120330 +3739,3,120340 +3740,3,120350 +3741,3,120360 +3742,3,120370 +3743,3,120380 +3744,3,120390 +3745,3,120400 +3746,3,120410 +3747,3,120420 +3748,3,120430 +3749,3,120450 +3750,3,120460 +3751,3,120550 +3752,3,120560 +3753,3,120570 +3754,3,120990 +3755,3,121000 +3756,3,121010 +3757,3,121020 +3758,4,140000 +3759,4,150010 +3760,4,150020 +3761,4,150030 +3762,4,150040 +3764,3,102000060 +3765,3,102000070 +3766,3,102000080 +3767,3,103000050 +3768,3,105000050 +3769,4,102000100 +3770,4,102000110 +3771,4,106000090 +3772,4,109000090 +3773,5,102000120 +3774,1,102000000 +3775,2,102000001 +3776,3,102000002 +3777,4,102000003 +3778,5,102000004 +3779,1,112000000 +3780,2,112000001 +3781,3,112000002 +3782,4,112000003 +3783,5,112000004 +3784,4,110024 +3785,4,110034 +3786,4,110044 +3787,4,110054 +3788,3,110060 +3789,3,110070 +3790,3,110080 +3791,3,110090 +3792,3,110100 +3793,3,110110 +3794,3,110120 +3795,3,110130 +3796,3,110140 +3797,3,110150 +3798,3,110160 +3799,3,110170 +3800,3,110180 +3801,3,110190 +3802,3,110200 +3803,3,110210 +3804,3,110220 +3805,3,110230 +3806,3,110240 +3807,3,110250 +3808,3,110260 +3809,3,110270 +3810,3,110620 +3811,3,110670 +3812,4,140000 +3813,4,150010 +3814,4,150020 +3815,4,150030 +3816,4,150040 +3818,3,104000060 +3819,3,104000070 +3820,3,104000080 +3821,3,102000050 +3822,4,104000100 +3823,4,104000110 +3824,4,107000090 +3825,5,104000120 +3826,1,111000000 +3827,2,111000001 +3828,3,111000002 +3829,4,111000003 +3830,5,111000004 +3831,1,120000000 +3832,2,120000001 +3833,3,120000002 +3834,4,120000003 +3835,5,120000004 +3836,4,110024 +3837,4,110034 +3838,4,110044 +3839,4,110054 +3840,3,110060 +3841,3,110070 +3842,3,110080 +3843,3,110090 +3844,3,110100 +3845,3,110110 +3846,3,110120 +3847,3,110130 +3848,3,110140 +3849,3,110150 +3850,3,110160 +3851,3,110170 +3852,3,110180 +3853,3,110190 +3854,3,110200 +3855,3,110210 +3856,3,110220 +3857,3,110230 +3858,3,110240 +3859,3,110250 +3860,3,110260 +3861,3,110270 +3862,3,110620 +3863,3,110670 +3864,4,140000 +3865,4,150010 +3866,4,150020 +3867,4,150030 +3868,4,150040 +3870,3,110000070 +3871,3,110000080 +3872,4,110000100 +3873,5,110000110 +3874,1,107000000 +3875,2,107000001 +3876,3,107000002 +3877,4,107000003 +3878,5,107000004 +3879,1,120000000 +3880,2,120000001 +3881,3,120000002 +3882,4,120000003 +3883,5,120000004 +3884,4,130024 +3885,4,130034 +3886,4,130044 +3887,4,130054 +3888,3,130060 +3889,3,130070 +3890,3,130080 +3891,3,130090 +3892,3,130100 +3893,3,130110 +3894,3,130120 +3895,3,130130 +3896,3,130140 +3897,3,130150 +3898,3,130160 +3899,3,130170 +3900,3,130180 +3901,3,130190 +3902,3,130200 +3903,3,130420 +3904,3,130510 +3905,3,130520 +3906,3,130530 +3907,3,130540 +3908,3,130660 +3909,4,140000 +3910,4,150010 +3911,4,150020 +3912,4,150030 +3913,4,150040 +3915,3,101000050 +3916,3,101000060 +3917,3,101000080 +3918,3,103000060 +3919,3,103000070 +3920,3,103000080 +3921,3,101000040 +3922,3,109000060 +3923,3,109000070 +3924,3,109000080 +3925,3,110000050 +3926,4,101000090 +3927,4,102000090 +3928,4,103000100 +3929,4,101000100 +3930,4,101000110 +3931,4,109000100 +3932,5,101000120 +3933,5,101000160 +3934,5,103000120 +3935,5,109000110 +3936,1,101000000 +3937,2,101000001 +3938,3,101000002 +3939,4,101000008 +3940,5,101000004 +3941,1,120000000 +3942,2,120000001 +3943,3,120000002 +3944,4,120000003 +3945,5,120000004 +3946,3,170004 +3948,3,107000060 +3949,3,107000070 +3950,3,107000080 +3951,3,108000050 +3952,3,109000050 +3953,4,107000100 +3954,4,103000090 +3955,5,107000110 +3956,1,105000000 +3957,2,105000001 +3958,3,105000002 +3959,4,105000003 +3960,5,105000004 +3961,1,120000000 +3962,2,120000001 +3963,3,120000002 +3964,4,120000003 +3965,5,120000004 +3966,3,170004 +3968,3,104000170 +3969,1,115000000 +3970,2,115000001 +3971,3,115000002 +3972,4,115000003 +3973,5,115000004 +3974,1,120000000 +3975,2,120000001 +3976,3,120000002 +3977,4,120000003 +3978,5,120000004 +3979,4,120024 +3980,4,120034 +3981,4,120044 +3982,4,120054 +3983,3,120241 +3984,3,120251 +3985,3,120261 +3986,3,120271 +3987,3,120300 +3988,3,120310 +3989,3,120320 +3990,3,120330 +3991,3,120340 +3992,3,120350 +3993,3,120360 +3994,3,120370 +3995,3,120380 +3996,3,120390 +3997,3,120400 +3998,3,120410 +3999,3,120420 +4000,3,120430 +4001,3,120450 +4002,3,120460 +4003,3,120550 +4004,3,120560 +4005,3,120570 +4006,3,120990 +4007,3,121000 +4008,3,121010 +4009,3,121020 +4010,4,140000 +4011,4,150010 +4012,4,150020 +4013,4,150030 +4014,4,150040 +4016,1,101000000 +4017,2,101000001 +4018,3,101000002 +4019,4,101000008 +4020,5,101000012 +4021,1,120000000 +4022,2,120000001 +4023,3,120000002 +4024,4,120000003 +4025,5,120000004 +4027,1,101000000 +4028,2,101000001 +4029,3,101000002 +4030,4,101000008 +4031,5,101000011 +4032,1,120000000 +4033,2,120000001 +4034,3,120000002 +4035,4,120000003 +4036,5,120000004 +4038,3,101000050 +4039,3,101000060 +4040,3,101000080 +4041,3,103000060 +4042,3,103000070 +4043,3,103000080 +4044,3,101000040 +4045,3,109000060 +4046,3,109000070 +4047,3,109000080 +4048,3,110000050 +4049,4,101000090 +4050,4,102000090 +4051,4,103000100 +4052,4,101000100 +4053,4,101000110 +4054,4,109000100 +4055,5,101000120 +4056,5,101000160 +4057,5,103000120 +4058,5,109000110 +4059,1,101000000 +4060,2,101000001 +4061,3,101000002 +4062,4,101000008 +4063,5,101000004 +4064,1,120000000 +4065,2,120000001 +4066,3,120000002 +4067,4,120000003 +4068,5,120000004 +4069,4,120024 +4070,4,120034 +4071,4,120044 +4072,4,120054 +4073,3,120241 +4074,3,120251 +4075,3,120261 +4076,3,120271 +4077,3,120300 +4078,3,120310 +4079,3,120320 +4080,3,120330 +4081,3,120340 +4082,3,120350 +4083,3,120360 +4084,3,120370 +4085,3,120380 +4086,3,120390 +4087,3,120400 +4088,3,120410 +4089,3,120420 +4090,3,120430 +4091,3,120450 +4092,3,120460 +4093,3,120550 +4094,3,120560 +4095,3,120570 +4096,3,120990 +4097,3,121000 +4098,3,121010 +4099,3,121020 +4100,4,140000 +4101,4,150010 +4102,4,150020 +4103,4,150030 +4104,4,150040 +4106,3,108000060 +4107,3,108000070 +4108,3,108000080 +4109,3,107000050 +4110,4,108000100 +4111,4,105000090 +4112,5,108000110 +4113,1,108000000 +4114,2,108000001 +4115,3,108000002 +4116,4,108000003 +4117,5,108000004 +4118,1,120000000 +4119,2,120000001 +4120,3,120000002 +4121,4,120000003 +4122,5,120000004 +4123,3,170004 +4125,3,102000060 +4126,3,102000070 +4127,3,102000080 +4128,3,103000050 +4129,3,105000050 +4130,4,102000100 +4131,4,102000110 +4132,4,106000090 +4133,4,109000090 +4134,5,102000120 +4135,1,102000000 +4136,2,102000001 +4137,3,102000002 +4138,4,102000003 +4139,5,102000004 +4140,1,112000000 +4141,2,112000001 +4142,3,112000002 +4143,4,112000003 +4144,5,112000004 +4145,4,110024 +4146,4,110034 +4147,4,110044 +4148,4,110054 +4149,3,110060 +4150,3,110070 +4151,3,110080 +4152,3,110090 +4153,3,110100 +4154,3,110110 +4155,3,110120 +4156,3,110130 +4157,3,110140 +4158,3,110150 +4159,3,110160 +4160,3,110170 +4161,3,110180 +4162,3,110190 +4163,3,110200 +4164,3,110210 +4165,3,110220 +4166,3,110230 +4167,3,110240 +4168,3,110250 +4169,3,110260 +4170,3,110270 +4171,3,110620 +4172,3,110670 +4173,4,140000 +4174,4,150010 +4175,4,150020 +4176,4,150030 +4177,4,150040 +4179,3,104000170 +4180,4,104000180 +4181,5,104000190 +4182,1,115000000 +4183,2,115000001 +4184,3,115000002 +4185,4,115000003 +4186,5,115000004 +4187,1,120000000 +4188,2,120000001 +4189,3,120000002 +4190,4,120000003 +4191,5,120000004 +4192,4,120024 +4193,4,120034 +4194,4,120044 +4195,4,120054 +4196,3,120241 +4197,3,120251 +4198,3,120261 +4199,3,120271 +4200,3,120300 +4201,3,120310 +4202,3,120320 +4203,3,120330 +4204,3,120340 +4205,3,120350 +4206,3,120360 +4207,3,120370 +4208,3,120380 +4209,3,120390 +4210,3,120400 +4211,3,120410 +4212,3,120420 +4213,3,120430 +4214,3,120450 +4215,3,120460 +4216,3,120550 +4217,3,120560 +4218,3,120570 +4219,3,120990 +4220,3,121000 +4221,3,121010 +4222,3,121020 +4223,4,140000 +4224,4,150010 +4225,4,150020 +4226,4,150030 +4227,4,150040 +4229,3,110000070 +4230,3,110000080 +4231,4,110000100 +4232,5,110000110 +4233,1,107000000 +4234,2,107000001 +4235,3,107000002 +4236,4,107000003 +4237,5,107000004 +4238,1,120000000 +4239,2,120000001 +4240,3,120000002 +4241,4,120000003 +4242,5,120000004 +4243,4,120024 +4244,4,120034 +4245,4,120044 +4246,4,120054 +4247,3,120241 +4248,3,120251 +4249,3,120261 +4250,3,120271 +4251,3,120300 +4252,3,120310 +4253,3,120320 +4254,3,120330 +4255,3,120340 +4256,3,120350 +4257,3,120360 +4258,3,120370 +4259,3,120380 +4260,3,120390 +4261,3,120400 +4262,3,120410 +4263,3,120420 +4264,3,120430 +4265,3,120450 +4266,3,120460 +4267,3,120550 +4268,3,120560 +4269,3,120570 +4270,3,120990 +4271,3,121000 +4272,3,121010 +4273,3,121020 +4274,4,140000 +4275,4,150010 +4276,4,150020 +4277,4,150030 +4278,4,150040 +4280,3,106000060 +4281,3,106000070 +4282,3,106000080 +4283,3,101000070 +4284,3,110000060 +4285,4,106000100 +4286,4,106000110 +4287,4,104000090 +4288,5,106000120 +4289,1,103000000 +4290,2,103000001 +4291,3,103000002 +4292,4,103000003 +4293,5,103000004 +4294,1,112000000 +4295,2,112000001 +4296,3,112000002 +4297,4,112000003 +4298,5,112000004 +4299,3,170004 +4301,3,105000060 +4302,3,105000070 +4303,3,105000080 +4304,3,104000050 +4305,3,106000050 +4306,4,105000100 +4307,4,105000110 +4308,4,108000090 +4309,4,110000090 +4310,5,105000120 +4311,1,109000000 +4312,2,109000001 +4313,3,109000002 +4314,4,109000003 +4315,5,109000004 +4316,1,112000000 +4317,2,112000001 +4318,3,112000002 +4319,4,112000003 +4320,5,112000004 +4321,4,130024 +4322,4,130034 +4323,4,130044 +4324,4,130054 +4325,3,130060 +4326,3,130070 +4327,3,130080 +4328,3,130090 +4329,3,130100 +4330,3,130110 +4331,3,130120 +4332,3,130130 +4333,3,130140 +4334,3,130150 +4335,3,130160 +4336,3,130170 +4337,3,130180 +4338,3,130190 +4339,3,130200 +4340,3,130420 +4341,3,130510 +4342,3,130520 +4343,3,130530 +4344,3,130540 +4345,3,130660 +4346,4,140000 +4347,4,150010 +4348,4,150020 +4349,4,150030 +4350,4,150040 +4352,3,101000050 +4353,3,101000060 +4354,3,101000080 +4355,3,103000060 +4356,3,103000070 +4357,3,103000080 +4358,3,101000040 +4359,3,109000060 +4360,3,109000070 +4361,3,109000080 +4362,3,110000050 +4363,4,101000090 +4364,4,102000090 +4365,4,103000100 +4366,4,101000100 +4367,4,101000110 +4368,4,109000100 +4369,5,101000120 +4370,5,103000120 +4371,5,109000110 +4372,1,101000000 +4373,2,101000001 +4374,3,101000002 +4375,4,101000008 +4376,5,101000004 +4377,1,112000000 +4378,2,112000001 +4379,3,112000002 +4380,4,112000003 +4381,5,112000004 +4382,3,180004 +4384,3,104000060 +4385,3,104000070 +4386,3,104000080 +4387,3,102000050 +4388,4,104000100 +4389,4,104000110 +4390,4,107000090 +4391,5,104000120 +4392,1,111000000 +4393,2,111000001 +4394,3,111000002 +4395,4,111000003 +4396,5,111000004 +4397,1,120000000 +4398,2,120000001 +4399,3,120000002 +4400,4,120000003 +4401,5,120000004 +4402,4,110024 +4403,4,110034 +4404,4,110044 +4405,4,110054 +4406,3,110060 +4407,3,110070 +4408,3,110080 +4409,3,110090 +4410,3,110100 +4411,3,110110 +4412,3,110120 +4413,3,110130 +4414,3,110140 +4415,3,110150 +4416,3,110160 +4417,3,110170 +4418,3,110180 +4419,3,110190 +4420,3,110200 +4421,3,110210 +4422,3,110220 +4423,3,110230 +4424,3,110240 +4425,3,110250 +4426,3,110260 +4427,3,110270 +4428,3,110620 +4429,3,110670 +4430,4,140000 +4431,4,150010 +4432,4,150020 +4433,4,150030 +4434,4,150040 +4436,3,107000060 +4437,3,107000070 +4438,3,107000080 +4439,3,108000050 +4440,3,109000050 +4441,4,107000100 +4442,4,103000090 +4443,5,107000110 +4444,1,105000000 +4445,2,105000001 +4446,3,105000002 +4447,4,105000003 +4448,5,105000004 +4449,1,120000000 +4450,2,120000001 +4451,3,120000002 +4452,4,120000003 +4453,5,120000004 +4454,4,130024 +4455,4,130034 +4456,4,130044 +4457,4,130054 +4458,3,130060 +4459,3,130070 +4460,3,130080 +4461,3,130090 +4462,3,130100 +4463,3,130110 +4464,3,130120 +4465,3,130130 +4466,3,130140 +4467,3,130150 +4468,3,130160 +4469,3,130170 +4470,3,130180 +4471,3,130190 +4472,3,130200 +4473,3,130420 +4474,3,130510 +4475,3,130520 +4476,3,130530 +4477,3,130540 +4478,3,130660 +4479,4,140000 +4480,4,150010 +4481,4,150020 +4482,4,150030 +4483,4,150040 +4485,1,109000010 +4486,2,109000020 +4487,2,109000030 +4488,2,109000040 +4489,3,109000050 +4490,3,109000060 +4491,3,109000070 +4492,3,109000080 +4493,4,109000090 +4494,4,109000100 +4495,5,109000110 +4496,1,170000 +4497,2,170001 +4498,3,170002 +4499,4,170003 +4500,5,170004 +4501,1,180000 +4502,2,180001 +4503,3,180002 +4504,4,180003 +4505,5,180004 +4506,1,201000010 +4507,1,292000010 +4508,1,299000040 +4509,1,299000070 +4510,1,299000110 +4511,1,299000120 +4512,1,299000140 +4513,2,202000010 +4514,2,290000010 +4515,2,299000010 +4516,2,299000150 +4517,2,299000190 +4518,2,299000200 +4519,2,299000210 +4520,3,298000050 +4521,3,298000060 +4522,3,299000060 +4523,3,299000170 +4524,4,140000 +4525,5,150010 +4526,5,150020 +4527,5,150030 +4528,5,150040 +4530,1,109000010 +4531,2,109000020 +4532,2,109000030 +4533,2,109000040 +4534,3,109000050 +4535,3,109000060 +4536,3,109000070 +4537,3,109000080 +4538,4,109000090 +4539,4,109000100 +4540,5,109000110 +4541,1,170000 +4542,2,170001 +4543,3,170002 +4544,4,170003 +4545,5,170004 +4546,1,180000 +4547,2,180001 +4548,3,180002 +4549,4,180003 +4550,5,180004 +4551,1,201000010 +4552,1,292000010 +4553,1,299000040 +4554,1,299000070 +4555,1,299000110 +4556,1,299000120 +4557,1,299000140 +4558,2,202000010 +4559,2,290000010 +4560,2,299000010 +4561,2,299000150 +4562,2,299000190 +4563,2,299000200 +4564,2,299000210 +4565,3,298000050 +4566,3,298000060 +4567,3,299000060 +4568,3,299000170 +4569,4,140000 +4570,5,150010 +4571,5,150020 +4572,5,150030 +4573,5,150040 +4575,3,109000050 +4576,3,109000060 +4577,3,109000070 +4578,3,109000080 +4579,4,109000090 +4580,4,109000100 +4581,5,109000110 +4582,1,170000 +4583,2,170001 +4584,3,170002 +4585,4,170003 +4586,5,170004 +4587,1,180000 +4588,2,180001 +4589,3,180002 +4590,4,180003 +4591,5,180004 +4592,1,201000010 +4593,1,292000010 +4594,1,299000040 +4595,1,299000070 +4596,1,299000110 +4597,1,299000120 +4598,1,299000140 +4599,2,202000010 +4600,2,290000010 +4601,2,299000010 +4602,2,299000150 +4603,2,299000190 +4604,2,299000200 +4605,2,299000210 +4606,3,298000050 +4607,3,298000060 +4608,3,299000060 +4609,3,299000170 +4610,4,140000 +4611,5,150010 +4612,5,150020 +4613,5,150030 +4614,5,150040 +4616,3,109000050 +4617,3,109000060 +4618,3,109000070 +4619,3,109000080 +4620,4,109000090 +4621,4,109000100 +4622,5,109000110 +4623,1,170000 +4624,2,170001 +4625,3,170002 +4626,4,170003 +4627,5,170004 +4628,1,180000 +4629,2,180001 +4630,3,180002 +4631,4,180003 +4632,5,180004 +4633,1,201000010 +4634,1,292000010 +4635,1,299000040 +4636,1,299000070 +4637,1,299000110 +4638,1,299000120 +4639,1,299000140 +4640,2,202000010 +4641,2,290000010 +4642,2,299000010 +4643,2,299000150 +4644,2,299000190 +4645,2,299000200 +4646,2,299000210 +4647,3,298000050 +4648,3,298000060 +4649,3,299000060 +4650,3,299000170 +4651,4,140000 +4652,5,150010 +4653,5,150020 +4654,5,150030 +4655,5,150040 +4657,3,109000050 +4658,3,109000060 +4659,3,109000070 +4660,3,109000080 +4661,4,109000090 +4662,4,109000100 +4663,5,109000110 +4664,1,170000 +4665,2,170001 +4666,3,170002 +4667,4,170003 +4668,5,170004 +4669,1,180000 +4670,2,180001 +4671,3,180002 +4672,4,180003 +4673,5,180004 +4674,1,201000010 +4675,1,292000010 +4676,1,299000040 +4677,1,299000070 +4678,1,299000110 +4679,1,299000120 +4680,1,299000140 +4681,2,202000010 +4682,2,290000010 +4683,2,299000010 +4684,2,299000150 +4685,2,299000190 +4686,2,299000200 +4687,2,299000210 +4688,3,298000050 +4689,3,298000060 +4690,3,299000060 +4691,3,299000170 +4692,4,140000 +4693,5,150010 +4694,5,150020 +4695,5,150030 +4696,5,150040 +4697,5,190000 +4698,5,200000 +4699,5,210000 +4701,3,102000060 +4702,3,102000070 +4703,3,102000080 +4704,3,103000050 +4705,3,105000050 +4706,4,102000100 +4707,4,102000110 +4708,4,106000090 +4709,4,109000090 +4710,5,102000120 +4711,1,102000000 +4712,2,102000001 +4713,3,102000002 +4714,4,102000003 +4715,5,102000004 +4716,1,112000000 +4717,2,112000001 +4718,3,112000002 +4719,4,112000003 +4720,5,112000004 +4721,3,170004 +4722,4,170005 +4724,3,110000070 +4725,3,110000080 +4726,4,110000100 +4727,5,110000110 +4728,1,107000000 +4729,2,107000001 +4730,3,107000002 +4731,4,107000003 +4732,5,107000004 +4733,1,120000000 +4734,2,120000001 +4735,3,120000002 +4736,4,120000003 +4737,5,120000004 +4738,4,120024 +4739,4,120034 +4740,4,120044 +4741,4,120054 +4742,3,120241 +4743,3,120251 +4744,3,120261 +4745,3,120271 +4746,3,120300 +4747,3,120310 +4748,3,120320 +4749,3,120330 +4750,3,120340 +4751,3,120350 +4752,3,120360 +4753,3,120370 +4754,3,120380 +4755,3,120390 +4756,3,120400 +4757,3,120410 +4758,3,120420 +4759,3,120430 +4760,3,120450 +4761,3,120460 +4762,3,120550 +4763,3,120560 +4764,3,120570 +4765,3,120990 +4766,3,121000 +4767,3,121010 +4768,3,121020 +4769,4,140000 +4770,4,150010 +4771,4,150020 +4772,4,150030 +4773,4,150040 +4775,3,105000060 +4776,3,105000070 +4777,3,105000080 +4778,3,104000050 +4779,3,106000050 +4780,4,105000100 +4781,4,105000110 +4782,4,108000090 +4783,4,110000090 +4784,5,105000120 +4785,1,109000000 +4786,2,109000001 +4787,3,109000002 +4788,4,109000003 +4789,5,109000004 +4790,1,112000000 +4791,2,112000001 +4792,3,112000002 +4793,4,112000003 +4794,5,112000004 +4795,4,130024 +4796,4,130034 +4797,4,130044 +4798,4,130054 +4799,3,130060 +4800,3,130070 +4801,3,130080 +4802,3,130090 +4803,3,130100 +4804,3,130110 +4805,3,130120 +4806,3,130130 +4807,3,130140 +4808,3,130150 +4809,3,130160 +4810,3,130170 +4811,3,130180 +4812,3,130190 +4813,3,130200 +4814,3,130420 +4815,3,130510 +4816,3,130520 +4817,3,130530 +4818,3,130540 +4819,3,130660 +4820,4,140000 +4821,4,150010 +4822,4,150020 +4823,4,150030 +4824,4,150040 +4826,3,104000060 +4827,3,104000070 +4828,3,104000080 +4829,3,102000050 +4830,4,104000100 +4831,4,104000110 +4832,4,107000090 +4833,5,104000120 +4834,1,111000000 +4835,2,111000001 +4836,3,111000002 +4837,4,111000003 +4838,5,111000004 +4839,1,120000000 +4840,2,120000001 +4841,3,120000002 +4842,4,120000003 +4843,5,120000004 +4844,4,110024 +4845,4,110034 +4846,4,110044 +4847,4,110054 +4848,3,110060 +4849,3,110070 +4850,3,110080 +4851,3,110090 +4852,3,110100 +4853,3,110110 +4854,3,110120 +4855,3,110130 +4856,3,110140 +4857,3,110150 +4858,3,110160 +4859,3,110170 +4860,3,110180 +4861,3,110190 +4862,3,110200 +4863,3,110210 +4864,3,110220 +4865,3,110230 +4866,3,110240 +4867,3,110250 +4868,3,110260 +4869,3,110270 +4870,3,110620 +4871,3,110670 +4872,4,140000 +4873,4,150010 +4874,4,150020 +4875,4,150030 +4876,4,150040 +4878,3,107000060 +4879,3,107000070 +4880,3,107000080 +4881,3,108000050 +4882,3,109000050 +4883,4,107000100 +4884,4,103000090 +4885,5,107000110 +4886,1,105000000 +4887,2,105000001 +4888,3,105000002 +4889,4,105000003 +4890,5,105000004 +4891,1,120000000 +4892,2,120000001 +4893,3,120000002 +4894,4,120000003 +4895,5,120000004 +4896,3,180004 +4897,4,180005 +4899,3,106000060 +4900,3,106000070 +4901,3,106000080 +4902,3,101000070 +4903,3,110000060 +4904,4,106000100 +4905,4,106000110 +4906,4,104000090 +4907,5,106000120 +4908,1,103000000 +4909,2,103000001 +4910,3,103000002 +4911,4,103000003 +4912,5,103000004 +4913,1,112000000 +4914,2,112000001 +4915,3,112000002 +4916,4,112000003 +4917,5,112000004 +4918,3,170004 +4919,4,170005 +4921,3,112000020 +4922,3,112000030 +4923,4,112000040 +4924,5,112000060 +4925,1,101000000 +4926,2,101000001 +4927,3,101000002 +4928,4,101000008 +4929,5,101000004 +4930,1,112000000 +4931,2,112000001 +4932,3,112000002 +4933,4,112000003 +4934,5,112000004 +4935,3,170004 +4936,4,170005 +4938,3,104000170 +4939,4,104000180 +4940,5,104000190 +4941,1,115000000 +4942,2,115000001 +4943,3,115000002 +4944,4,115000003 +4945,5,115000004 +4946,1,120000000 +4947,2,120000001 +4948,3,120000002 +4949,4,120000003 +4950,5,120000004 +4951,4,120024 +4952,4,120034 +4953,4,120044 +4954,4,120054 +4955,3,120241 +4956,3,120251 +4957,3,120261 +4958,3,120271 +4959,3,120300 +4960,3,120310 +4961,3,120320 +4962,3,120330 +4963,3,120340 +4964,3,120350 +4965,3,120360 +4966,3,120370 +4967,3,120380 +4968,3,120390 +4969,3,120400 +4970,3,120410 +4971,3,120420 +4972,3,120430 +4973,3,120450 +4974,3,120460 +4975,3,120550 +4976,3,120560 +4977,3,120570 +4978,3,120990 +4979,3,121000 +4980,3,121010 +4981,3,121020 +4982,4,140000 +4983,4,150010 +4984,4,150020 +4985,4,150030 +4986,4,150040 +4988,3,111000010 +4989,3,111000020 +4990,3,111000030 +4991,4,111000040 +4992,5,111000060 +4993,1,101000000 +4994,2,101000001 +4995,3,101000002 +4996,4,101000008 +4997,5,101000004 +4998,1,120000000 +4999,2,120000001 +5000,3,120000002 +5001,4,120000003 +5002,5,120000004 +5003,4,110024 +5004,4,110034 +5005,4,110044 +5006,4,110054 +5007,3,110060 +5008,3,110070 +5009,3,110080 +5010,3,110090 +5011,3,110100 +5012,3,110110 +5013,3,110120 +5014,3,110130 +5015,3,110140 +5016,3,110150 +5017,3,110160 +5018,3,110170 +5019,3,110180 +5020,3,110190 +5021,3,110200 +5022,3,110210 +5023,3,110220 +5024,3,110230 +5025,3,110240 +5026,3,110250 +5027,3,110260 +5028,3,110270 +5029,3,110620 +5030,3,110670 +5031,4,140000 +5032,4,150010 +5033,4,150020 +5034,4,150030 +5035,4,150040 +5037,3,108000060 +5038,3,108000070 +5039,3,108000080 +5040,3,107000050 +5041,3,112000010 +5042,4,108000100 +5043,4,105000090 +5044,5,108000110 +5045,1,108000000 +5046,2,108000001 +5047,3,108000002 +5048,4,108000003 +5049,5,108000004 +5050,1,120000000 +5051,2,120000001 +5052,3,120000002 +5053,4,120000003 +5054,5,120000004 +5055,4,130024 +5056,4,130034 +5057,4,130044 +5058,4,130054 +5059,3,130060 +5060,3,130070 +5061,3,130080 +5062,3,130090 +5063,3,130100 +5064,3,130110 +5065,3,130120 +5066,3,130130 +5067,3,130140 +5068,3,130150 +5069,3,130160 +5070,3,130170 +5071,3,130180 +5072,3,130190 +5073,3,130200 +5074,3,130420 +5075,3,130510 +5076,3,130520 +5077,3,130530 +5078,3,130540 +5079,3,130660 +5080,4,140000 +5081,4,150010 +5082,4,150020 +5083,4,150030 +5084,4,150040 +5086,3,111000010 +5087,3,111000020 +5088,3,111000030 +5089,4,111000040 +5090,5,111000060 +5091,1,170002 +5092,2,170003 +5093,3,170004 +5094,1,180002 +5095,2,180003 +5096,3,180004 +5097,1,201000010 +5098,1,292000010 +5099,1,299000040 +5100,1,299000070 +5101,1,299000110 +5102,1,299000120 +5103,1,299000140 +5104,2,202000010 +5105,2,290000010 +5106,2,299000010 +5107,2,299000150 +5108,2,299000190 +5109,2,299000200 +5110,2,299000210 +5111,3,298000050 +5112,3,298000060 +5113,3,299000060 +5114,3,299000170 +5115,4,140000 +5116,5,150010 +5117,5,150020 +5118,5,150030 +5119,5,150040 +5121,3,111000010 +5122,3,111000020 +5123,3,111000030 +5124,4,111000040 +5125,5,111000060 +5126,2,170003 +5127,3,170004 +5128,2,180003 +5129,3,180004 +5130,1,201000010 +5131,1,292000010 +5132,1,299000040 +5133,1,299000070 +5134,1,299000110 +5135,1,299000120 +5136,1,299000140 +5137,2,202000010 +5138,2,290000010 +5139,2,299000010 +5140,2,299000150 +5141,2,299000190 +5142,2,299000200 +5143,2,299000210 +5144,3,298000050 +5145,3,298000060 +5146,3,299000060 +5147,3,299000170 +5148,4,140000 +5149,5,150010 +5150,5,150020 +5151,5,150030 +5152,5,150040 +5154,3,111000010 +5155,3,111000020 +5156,3,111000030 +5157,4,111000040 +5158,5,111000060 +5159,3,170004 +5160,3,180004 +5161,1,201000010 +5162,1,292000010 +5163,1,299000040 +5164,1,299000070 +5165,1,299000110 +5166,1,299000120 +5167,1,299000140 +5168,2,202000010 +5169,2,290000010 +5170,2,299000010 +5171,2,299000150 +5172,2,299000190 +5173,2,299000200 +5174,2,299000210 +5175,3,298000050 +5176,3,298000060 +5177,3,299000060 +5178,3,299000170 +5179,4,140000 +5180,5,150010 +5181,5,150020 +5182,5,150030 +5183,5,150040 +5185,3,111000010 +5186,3,111000020 +5187,3,111000030 +5188,4,111000040 +5189,5,111000060 +5190,3,170004 +5191,3,180004 +5192,1,201000010 +5193,1,292000010 +5194,1,299000040 +5195,1,299000070 +5196,1,299000110 +5197,1,299000120 +5198,1,299000140 +5199,2,202000010 +5200,2,290000010 +5201,2,299000010 +5202,2,299000150 +5203,2,299000190 +5204,2,299000200 +5205,2,299000210 +5206,3,298000050 +5207,3,298000060 +5208,3,299000060 +5209,3,299000170 +5210,4,140000 +5211,5,150010 +5212,5,150020 +5213,5,150030 +5214,5,150040 +5216,3,111000010 +5217,3,111000020 +5218,3,111000030 +5219,4,111000040 +5220,5,111000060 +5221,3,170004 +5222,3,180004 +5223,1,201000010 +5224,1,292000010 +5225,1,299000040 +5226,1,299000070 +5227,1,299000110 +5228,1,299000120 +5229,1,299000140 +5230,2,202000010 +5231,2,290000010 +5232,2,299000010 +5233,2,299000150 +5234,2,299000190 +5235,2,299000200 +5236,2,299000210 +5237,3,298000050 +5238,3,298000060 +5239,3,299000060 +5240,3,299000170 +5241,4,140000 +5242,5,150010 +5243,5,150020 +5244,5,150030 +5245,5,150040 +5246,5,190000 +5247,5,200000 +5248,5,210000 +5250,3,101000050 +5251,3,101000060 +5252,3,101000080 +5253,3,101000040 +5254,3,109000060 +5255,3,109000070 +5256,3,109000080 +5257,3,105000060 +5258,3,105000070 +5259,3,105000080 +5260,3,104000050 +5261,3,106000050 +5262,4,101000090 +5263,4,101000100 +5264,4,101000110 +5265,4,109000100 +5266,4,105000100 +5267,4,105000110 +5268,4,108000090 +5269,4,110000090 +5270,5,101000120 +5271,5,109000110 +5272,5,105000120 +5273,1,101000000 +5274,2,101000001 +5275,3,101000002 +5276,4,101000008 +5277,5,101000004 +5278,1,109000000 +5279,2,109000001 +5280,3,109000002 +5281,4,109000003 +5282,5,109000004 +5283,3,170004 +5284,4,170005 +5285,3,180004 +5286,4,180005 +5287,4,140000 +5288,4,150010 +5289,4,150020 +5290,4,150030 +5291,4,150040 +5293,3,102000060 +5294,3,102000070 +5295,3,102000080 +5296,3,103000050 +5297,3,105000050 +5298,3,107000060 +5299,3,107000070 +5300,3,107000080 +5301,3,108000050 +5302,3,109000050 +5303,3,103000060 +5304,3,103000070 +5305,3,103000080 +5306,3,110000050 +5307,4,102000100 +5308,4,102000110 +5309,4,106000090 +5310,4,109000090 +5311,4,107000100 +5312,4,103000090 +5313,4,102000090 +5314,4,103000100 +5315,5,102000120 +5316,5,107000110 +5317,5,103000120 +5318,1,102000000 +5319,2,102000001 +5320,3,102000002 +5321,4,102000003 +5322,5,102000004 +5323,1,105000000 +5324,2,105000001 +5325,3,105000002 +5326,4,105000003 +5327,5,105000004 +5328,1,112000000 +5329,2,112000001 +5330,3,112000002 +5331,4,112000003 +5332,5,112000004 +5333,4,110024 +5334,4,110034 +5335,4,110044 +5336,4,110054 +5337,3,110060 +5338,3,110070 +5339,3,110080 +5340,3,110090 +5341,3,110100 +5342,3,110110 +5343,3,110120 +5344,3,110130 +5345,3,110140 +5346,3,110150 +5347,3,110160 +5348,3,110170 +5349,3,110180 +5350,3,110190 +5351,3,110200 +5352,3,110210 +5353,3,110220 +5354,3,110230 +5355,3,110240 +5356,3,110250 +5357,3,110260 +5358,3,110270 +5359,3,110620 +5360,3,110670 +5361,4,140000 +5362,4,150010 +5363,4,150020 +5364,4,150030 +5365,4,150040 +5367,3,106000060 +5368,3,106000070 +5369,3,106000080 +5370,3,101000070 +5371,3,110000060 +5372,3,104000060 +5373,3,104000070 +5374,3,104000080 +5375,3,102000050 +5376,3,104000170 +5377,3,104000260 +5378,4,106000100 +5379,4,106000110 +5380,4,104000090 +5381,4,104000100 +5382,4,104000110 +5383,4,107000090 +5384,4,104000180 +5385,4,104000270 +5386,5,106000120 +5387,5,104000120 +5388,5,104000190 +5389,1,103000000 +5390,2,103000001 +5391,3,103000002 +5392,4,103000003 +5393,5,103000004 +5394,1,111000000 +5395,2,111000001 +5396,3,111000002 +5397,4,111000003 +5398,5,111000004 +5399,1,115000000 +5400,2,115000001 +5401,3,115000002 +5402,4,115000003 +5403,5,115000004 +5404,4,120024 +5405,4,120034 +5406,4,120044 +5407,4,120054 +5408,3,120241 +5409,3,120251 +5410,3,120261 +5411,3,120271 +5412,3,120300 +5413,3,120310 +5414,3,120320 +5415,3,120330 +5416,3,120340 +5417,3,120350 +5418,3,120360 +5419,3,120370 +5420,3,120380 +5421,3,120390 +5422,3,120400 +5423,3,120410 +5424,3,120420 +5425,3,120430 +5426,3,120450 +5427,3,120460 +5428,3,120550 +5429,3,120560 +5430,3,120570 +5431,3,120990 +5432,3,121000 +5433,3,121010 +5434,3,121020 +5435,4,140000 +5436,4,150010 +5437,4,150020 +5438,4,150030 +5439,4,150040 +5441,3,111000010 +5442,3,111000020 +5443,3,111000030 +5444,3,112000020 +5445,3,112000030 +5446,3,108000060 +5447,3,108000070 +5448,3,108000080 +5449,3,107000050 +5450,3,112000010 +5451,3,110000070 +5452,3,110000080 +5453,4,111000040 +5454,4,112000040 +5455,4,108000100 +5456,4,105000090 +5457,4,110000100 +5458,5,111000060 +5459,5,112000060 +5460,5,108000110 +5461,5,110000110 +5462,1,108000000 +5463,2,108000001 +5464,3,108000002 +5465,4,108000003 +5466,5,108000004 +5467,1,107000000 +5468,2,107000001 +5469,3,107000002 +5470,4,107000003 +5471,5,107000004 +5472,1,120000000 +5473,2,120000001 +5474,3,120000002 +5475,4,120000003 +5476,5,120000004 +5477,4,130024 +5478,4,130034 +5479,4,130044 +5480,4,130054 +5481,3,130060 +5482,3,130070 +5483,3,130080 +5484,3,130090 +5485,3,130100 +5486,3,130110 +5487,3,130120 +5488,3,130130 +5489,3,130140 +5490,3,130150 +5491,3,130160 +5492,3,130170 +5493,3,130180 +5494,3,130190 +5495,3,130200 +5496,3,130420 +5497,3,130510 +5498,3,130520 +5499,3,130530 +5500,3,130540 +5501,3,130660 +5502,4,140000 +5503,4,150010 +5504,4,150020 +5505,4,150030 +5506,4,150040 +5508,1,101000000 +5509,2,101000001 +5510,3,101000002 +5511,4,101000008 +5512,5,101000004 +5513,1,120000000 +5514,2,120000001 +5515,3,120000002 +5516,4,120000003 +5517,5,120000004 +5519,1,101000010 +5520,1,102000010 +5521,1,103000010 +5522,1,104000010 +5523,1,105000010 +5524,1,106000010 +5525,1,107000010 +5526,1,108000010 +5527,1,109000010 +5528,1,110000010 +5529,2,101000020 +5530,2,101000030 +5531,2,102000020 +5532,2,102000030 +5533,2,102000040 +5534,2,103000020 +5535,2,103000030 +5536,2,103000040 +5537,2,104000020 +5538,2,104000030 +5539,2,104000040 +5540,2,105000020 +5541,2,105000030 +5542,2,105000040 +5543,2,106000020 +5544,2,106000030 +5545,2,106000040 +5546,2,107000020 +5547,2,107000030 +5548,2,107000040 +5549,2,108000020 +5550,2,108000030 +5551,2,108000040 +5552,2,109000020 +5553,2,109000030 +5554,2,109000040 +5555,2,110000020 +5556,2,110000030 +5557,2,110000040 +5558,3,101000050 +5559,3,101000060 +5560,3,101000080 +5561,3,101000040 +5562,3,109000060 +5563,3,109000070 +5564,3,109000080 +5565,3,105000060 +5566,3,105000070 +5567,3,105000080 +5568,3,104000050 +5569,3,106000050 +5570,3,102000060 +5571,3,102000070 +5572,3,102000080 +5573,3,103000050 +5574,3,105000050 +5575,3,107000060 +5576,3,107000070 +5577,3,107000080 +5578,3,108000050 +5579,3,109000050 +5580,3,103000060 +5581,3,103000070 +5582,3,103000080 +5583,3,110000050 +5584,3,106000060 +5585,3,106000070 +5586,3,106000080 +5587,3,101000070 +5588,3,110000060 +5589,3,104000060 +5590,3,104000070 +5591,3,104000080 +5592,3,102000050 +5593,3,104000170 +5594,3,104000260 +5595,3,111000010 +5596,3,111000020 +5597,3,111000030 +5598,3,112000020 +5599,3,112000030 +5600,3,108000060 +5601,3,108000070 +5602,3,108000080 +5603,3,107000050 +5604,3,112000010 +5605,3,110000070 +5606,3,110000080 +5607,4,101000090 +5608,4,101000100 +5609,4,101000110 +5610,4,109000100 +5611,4,105000100 +5612,4,105000110 +5613,4,108000090 +5614,4,110000090 +5615,4,102000100 +5616,4,102000110 +5617,4,106000090 +5618,4,109000090 +5619,4,107000100 +5620,4,103000090 +5621,4,102000090 +5622,4,103000100 +5623,4,106000100 +5624,4,106000110 +5625,4,104000090 +5626,4,104000100 +5627,4,104000110 +5628,4,107000090 +5629,4,104000180 +5630,4,111000040 +5631,4,112000040 +5632,4,108000100 +5633,4,105000090 +5634,4,110000100 +5635,5,101000120 +5636,5,109000110 +5637,5,105000120 +5638,5,102000120 +5639,5,107000110 +5640,5,103000120 +5641,5,106000120 +5642,5,104000120 +5643,5,104000190 +5644,5,111000060 +5645,5,112000060 +5646,5,108000110 +5647,5,110000110 +5648,1,170002 +5649,2,170003 +5650,3,170004 +5651,1,180002 +5652,2,180003 +5653,3,180004 +5654,1,201000010 +5655,1,292000010 +5656,1,299000040 +5657,1,299000070 +5658,1,299000110 +5659,1,299000120 +5660,1,299000140 +5661,2,202000010 +5662,2,290000010 +5663,2,299000010 +5664,2,299000150 +5665,2,299000190 +5666,2,299000200 +5667,2,299000210 +5668,3,298000050 +5669,3,298000060 +5670,3,299000060 +5671,3,299000170 +5672,5,297000100 +5673,5,291000020 +5674,4,140000 +5675,5,150010 +5676,5,150020 +5677,5,150030 +5678,5,150040 +5680,1,101000010 +5681,1,102000010 +5682,1,103000010 +5683,1,104000010 +5684,1,105000010 +5685,1,106000010 +5686,1,107000010 +5687,1,108000010 +5688,1,109000010 +5689,1,110000010 +5690,2,101000020 +5691,2,101000030 +5692,2,102000020 +5693,2,102000030 +5694,2,102000040 +5695,2,103000020 +5696,2,103000030 +5697,2,103000040 +5698,2,104000020 +5699,2,104000030 +5700,2,104000040 +5701,2,105000020 +5702,2,105000030 +5703,2,105000040 +5704,2,106000020 +5705,2,106000030 +5706,2,106000040 +5707,2,107000020 +5708,2,107000030 +5709,2,107000040 +5710,2,108000020 +5711,2,108000030 +5712,2,108000040 +5713,2,109000020 +5714,2,109000030 +5715,2,109000040 +5716,2,110000020 +5717,2,110000030 +5718,2,110000040 +5719,3,101000050 +5720,3,101000060 +5721,3,101000080 +5722,3,101000040 +5723,3,109000060 +5724,3,109000070 +5725,3,109000080 +5726,3,105000060 +5727,3,105000070 +5728,3,105000080 +5729,3,104000050 +5730,3,106000050 +5731,3,102000060 +5732,3,102000070 +5733,3,102000080 +5734,3,103000050 +5735,3,105000050 +5736,3,107000060 +5737,3,107000070 +5738,3,107000080 +5739,3,108000050 +5740,3,109000050 +5741,3,103000060 +5742,3,103000070 +5743,3,103000080 +5744,3,110000050 +5745,3,106000060 +5746,3,106000070 +5747,3,106000080 +5748,3,101000070 +5749,3,110000060 +5750,3,104000060 +5751,3,104000070 +5752,3,104000080 +5753,3,102000050 +5754,3,104000170 +5755,3,104000260 +5756,3,111000010 +5757,3,111000020 +5758,3,111000030 +5759,3,112000020 +5760,3,112000030 +5761,3,108000060 +5762,3,108000070 +5763,3,108000080 +5764,3,107000050 +5765,3,112000010 +5766,3,110000070 +5767,3,110000080 +5768,4,101000090 +5769,4,101000100 +5770,4,101000110 +5771,4,109000100 +5772,4,105000100 +5773,4,105000110 +5774,4,108000090 +5775,4,110000090 +5776,4,102000100 +5777,4,102000110 +5778,4,106000090 +5779,4,109000090 +5780,4,107000100 +5781,4,103000090 +5782,4,102000090 +5783,4,103000100 +5784,4,106000100 +5785,4,106000110 +5786,4,104000090 +5787,4,104000100 +5788,4,104000110 +5789,4,107000090 +5790,4,104000180 +5791,4,111000040 +5792,4,112000040 +5793,4,108000100 +5794,4,105000090 +5795,4,110000100 +5796,5,101000120 +5797,5,109000110 +5798,5,105000120 +5799,5,102000120 +5800,5,107000110 +5801,5,103000120 +5802,5,106000120 +5803,5,104000120 +5804,5,104000190 +5805,5,111000060 +5806,5,112000060 +5807,5,108000110 +5808,5,110000110 +5809,2,170003 +5810,3,170004 +5811,2,180003 +5812,3,180004 +5813,1,201000010 +5814,1,292000010 +5815,1,299000040 +5816,1,299000070 +5817,1,299000110 +5818,1,299000120 +5819,1,299000140 +5820,2,202000010 +5821,2,290000010 +5822,2,299000010 +5823,2,299000150 +5824,2,299000190 +5825,2,299000200 +5826,2,299000210 +5827,3,298000050 +5828,3,298000060 +5829,3,299000060 +5830,3,299000170 +5831,5,297000100 +5832,5,291000020 +5833,4,140000 +5834,5,150010 +5835,5,150020 +5836,5,150030 +5837,5,150040 +5839,3,101000050 +5840,3,101000060 +5841,3,101000080 +5842,3,101000040 +5843,3,109000060 +5844,3,109000070 +5845,3,109000080 +5846,3,105000060 +5847,3,105000070 +5848,3,105000080 +5849,3,104000050 +5850,3,106000050 +5851,3,102000060 +5852,3,102000070 +5853,3,102000080 +5854,3,103000050 +5855,3,105000050 +5856,3,107000060 +5857,3,107000070 +5858,3,107000080 +5859,3,108000050 +5860,3,109000050 +5861,3,103000060 +5862,3,103000070 +5863,3,103000080 +5864,3,110000050 +5865,3,106000060 +5866,3,106000070 +5867,3,106000080 +5868,3,101000070 +5869,3,110000060 +5870,3,104000060 +5871,3,104000070 +5872,3,104000080 +5873,3,102000050 +5874,3,104000170 +5875,3,104000260 +5876,3,111000010 +5877,3,111000020 +5878,3,111000030 +5879,3,112000020 +5880,3,112000030 +5881,3,108000060 +5882,3,108000070 +5883,3,108000080 +5884,3,107000050 +5885,3,112000010 +5886,3,110000070 +5887,3,110000080 +5888,4,101000090 +5889,4,101000100 +5890,4,101000110 +5891,4,109000100 +5892,4,105000100 +5893,4,105000110 +5894,4,108000090 +5895,4,110000090 +5896,4,102000100 +5897,4,102000110 +5898,4,106000090 +5899,4,109000090 +5900,4,107000100 +5901,4,103000090 +5902,4,102000090 +5903,4,103000100 +5904,4,106000100 +5905,4,106000110 +5906,4,104000090 +5907,4,104000100 +5908,4,104000110 +5909,4,107000090 +5910,4,104000180 +5911,4,111000040 +5912,4,112000040 +5913,4,108000100 +5914,4,105000090 +5915,4,110000100 +5916,5,101000120 +5917,5,109000110 +5918,5,105000120 +5919,5,102000120 +5920,5,107000110 +5921,5,103000120 +5922,5,106000120 +5923,5,104000120 +5924,5,104000190 +5925,5,111000060 +5926,5,112000060 +5927,5,108000110 +5928,5,110000110 +5929,3,170004 +5930,3,180004 +5931,1,201000010 +5932,1,292000010 +5933,1,299000040 +5934,1,299000070 +5935,1,299000110 +5936,1,299000120 +5937,1,299000140 +5938,2,202000010 +5939,2,290000010 +5940,2,299000010 +5941,2,299000150 +5942,2,299000190 +5943,2,299000200 +5944,2,299000210 +5945,3,298000050 +5946,3,298000060 +5947,3,299000060 +5948,3,299000170 +5949,5,297000100 +5950,5,291000020 +5951,4,140000 +5952,5,150010 +5953,5,150020 +5954,5,150030 +5955,5,150040 +5957,3,101000050 +5958,3,101000060 +5959,3,101000080 +5960,3,101000040 +5961,3,109000060 +5962,3,109000070 +5963,3,109000080 +5964,3,105000060 +5965,3,105000070 +5966,3,105000080 +5967,3,104000050 +5968,3,106000050 +5969,3,102000060 +5970,3,102000070 +5971,3,102000080 +5972,3,103000050 +5973,3,105000050 +5974,3,107000060 +5975,3,107000070 +5976,3,107000080 +5977,3,108000050 +5978,3,109000050 +5979,3,103000060 +5980,3,103000070 +5981,3,103000080 +5982,3,110000050 +5983,3,106000060 +5984,3,106000070 +5985,3,106000080 +5986,3,101000070 +5987,3,110000060 +5988,3,104000060 +5989,3,104000070 +5990,3,104000080 +5991,3,102000050 +5992,3,104000170 +5993,3,104000260 +5994,3,111000010 +5995,3,111000020 +5996,3,111000030 +5997,3,112000020 +5998,3,112000030 +5999,3,108000060 +6000,3,108000070 +6001,3,108000080 +6002,3,107000050 +6003,3,112000010 +6004,3,110000070 +6005,3,110000080 +6006,4,101000090 +6007,4,101000100 +6008,4,101000110 +6009,4,109000100 +6010,4,105000100 +6011,4,105000110 +6012,4,108000090 +6013,4,110000090 +6014,4,102000100 +6015,4,102000110 +6016,4,106000090 +6017,4,109000090 +6018,4,107000100 +6019,4,103000090 +6020,4,102000090 +6021,4,103000100 +6022,4,106000100 +6023,4,106000110 +6024,4,104000090 +6025,4,104000100 +6026,4,104000110 +6027,4,107000090 +6028,4,104000180 +6029,4,111000040 +6030,4,112000040 +6031,4,108000100 +6032,4,105000090 +6033,4,110000100 +6034,5,101000120 +6035,5,109000110 +6036,5,105000120 +6037,5,102000120 +6038,5,107000110 +6039,5,103000120 +6040,5,106000120 +6041,5,104000120 +6042,5,104000190 +6043,5,111000060 +6044,5,112000060 +6045,5,108000110 +6046,5,110000110 +6047,3,170004 +6048,3,180004 +6049,1,201000010 +6050,1,292000010 +6051,1,299000040 +6052,1,299000070 +6053,1,299000110 +6054,1,299000120 +6055,1,299000140 +6056,2,202000010 +6057,2,290000010 +6058,2,299000010 +6059,2,299000150 +6060,2,299000190 +6061,2,299000200 +6062,2,299000210 +6063,3,298000050 +6064,3,298000060 +6065,3,299000060 +6066,3,299000170 +6067,5,297000100 +6068,5,291000020 +6069,4,140000 +6070,5,150010 +6071,5,150020 +6072,5,150030 +6073,5,150040 +6075,3,101000050 +6076,3,101000060 +6077,3,101000080 +6078,3,101000040 +6079,3,109000060 +6080,3,109000070 +6081,3,109000080 +6082,3,105000060 +6083,3,105000070 +6084,3,105000080 +6085,3,104000050 +6086,3,106000050 +6087,3,102000060 +6088,3,102000070 +6089,3,102000080 +6090,3,103000050 +6091,3,105000050 +6092,3,107000060 +6093,3,107000070 +6094,3,107000080 +6095,3,108000050 +6096,3,109000050 +6097,3,103000060 +6098,3,103000070 +6099,3,103000080 +6100,3,110000050 +6101,3,106000060 +6102,3,106000070 +6103,3,106000080 +6104,3,101000070 +6105,3,110000060 +6106,3,104000060 +6107,3,104000070 +6108,3,104000080 +6109,3,102000050 +6110,3,104000170 +6111,3,104000260 +6112,3,111000010 +6113,3,111000020 +6114,3,111000030 +6115,3,112000020 +6116,3,112000030 +6117,3,108000060 +6118,3,108000070 +6119,3,108000080 +6120,3,107000050 +6121,3,112000010 +6122,3,110000070 +6123,3,110000080 +6124,4,101000090 +6125,4,101000100 +6126,4,101000110 +6127,4,109000100 +6128,4,105000100 +6129,4,105000110 +6130,4,108000090 +6131,4,110000090 +6132,4,102000100 +6133,4,102000110 +6134,4,106000090 +6135,4,109000090 +6136,4,107000100 +6137,4,103000090 +6138,4,102000090 +6139,4,103000100 +6140,4,106000100 +6141,4,106000110 +6142,4,104000090 +6143,4,104000100 +6144,4,104000110 +6145,4,107000090 +6146,4,104000180 +6147,4,111000040 +6148,4,112000040 +6149,4,108000100 +6150,4,105000090 +6151,4,110000100 +6152,5,101000120 +6153,5,109000110 +6154,5,105000120 +6155,5,102000120 +6156,5,107000110 +6157,5,103000120 +6158,5,106000120 +6159,5,104000120 +6160,5,104000190 +6161,5,111000060 +6162,5,112000060 +6163,5,108000110 +6164,5,110000110 +6165,3,170004 +6166,3,180004 +6167,1,201000010 +6168,1,292000010 +6169,1,299000040 +6170,1,299000070 +6171,1,299000110 +6172,1,299000120 +6173,1,299000140 +6174,2,202000010 +6175,2,290000010 +6176,2,299000010 +6177,2,299000150 +6178,2,299000190 +6179,2,299000200 +6180,2,299000210 +6181,3,298000050 +6182,3,298000060 +6183,3,299000060 +6184,3,299000170 +6185,5,297000100 +6186,5,291000020 +6187,4,140000 +6188,5,150010 +6189,5,150020 +6190,5,150030 +6191,5,150040 +6192,5,190000 +6193,5,200000 +6194,5,210000 +6196,3,111000010 +6197,3,111000020 +6198,3,111000030 +6199,3,112000020 +6200,3,112000030 +6201,3,108000060 +6202,3,108000070 +6203,3,108000080 +6204,3,107000050 +6205,3,112000010 +6206,3,110000070 +6207,3,110000080 +6208,4,111000040 +6209,4,112000040 +6210,4,108000100 +6211,4,105000090 +6212,4,110000100 +6213,5,111000060 +6214,5,112000060 +6215,5,108000110 +6216,5,110000110 +6217,1,108000000 +6218,2,108000001 +6219,3,108000002 +6220,4,108000003 +6221,5,108000004 +6222,1,107000000 +6223,2,107000001 +6224,3,107000002 +6225,4,107000003 +6226,5,107000004 +6227,1,120000000 +6228,2,120000001 +6229,3,120000002 +6230,4,120000003 +6231,5,120000004 +6232,3,170004 +6233,4,170005 +6234,3,180004 +6235,4,180005 +6236,4,140000 +6237,4,150010 +6238,4,150020 +6239,4,150030 +6240,4,150040 +6242,3,101000050 +6243,3,101000060 +6244,3,101000080 +6245,3,101000040 +6246,3,109000060 +6247,3,109000070 +6248,3,109000080 +6249,3,105000060 +6250,3,105000070 +6251,3,105000080 +6252,3,104000050 +6253,3,106000050 +6254,4,101000090 +6255,4,101000100 +6256,4,101000110 +6257,4,109000100 +6258,4,105000100 +6259,4,105000110 +6260,4,108000090 +6261,4,110000090 +6262,5,101000120 +6263,5,109000110 +6264,5,105000120 +6265,1,101000000 +6266,2,101000001 +6267,3,101000002 +6268,4,101000008 +6269,5,101000004 +6270,1,109000000 +6271,2,109000001 +6272,3,109000002 +6273,4,109000003 +6274,5,109000004 +6275,4,110024 +6276,4,110034 +6277,4,110044 +6278,4,110054 +6279,3,110060 +6280,3,110070 +6281,3,110080 +6282,3,110090 +6283,3,110100 +6284,3,110110 +6285,3,110120 +6286,3,110130 +6287,3,110140 +6288,3,110150 +6289,3,110160 +6290,3,110170 +6291,3,110180 +6292,3,110190 +6293,3,110200 +6294,3,110210 +6295,3,110220 +6296,3,110230 +6297,3,110240 +6298,3,110250 +6299,3,110260 +6300,3,110270 +6301,3,110620 +6302,3,110670 +6303,4,140000 +6304,4,150010 +6305,4,150020 +6306,4,150030 +6307,4,150040 +6309,3,102000060 +6310,3,102000070 +6311,3,102000080 +6312,3,103000050 +6313,3,105000050 +6314,3,107000060 +6315,3,107000070 +6316,3,107000080 +6317,3,108000050 +6318,3,109000050 +6319,3,103000060 +6320,3,103000070 +6321,3,103000080 +6322,3,110000050 +6323,4,102000100 +6324,4,102000110 +6325,4,106000090 +6326,4,109000090 +6327,4,107000100 +6328,4,103000090 +6329,4,102000090 +6330,4,103000100 +6331,5,102000120 +6332,5,107000110 +6333,5,103000120 +6334,1,102000000 +6335,2,102000001 +6336,3,102000002 +6337,4,102000003 +6338,5,102000004 +6339,1,105000000 +6340,2,105000001 +6341,3,105000002 +6342,4,105000003 +6343,5,105000004 +6344,1,112000000 +6345,2,112000001 +6346,3,112000002 +6347,4,112000003 +6348,5,112000004 +6349,4,120024 +6350,4,120034 +6351,4,120044 +6352,4,120054 +6353,3,120241 +6354,3,120251 +6355,3,120261 +6356,3,120271 +6357,3,120300 +6358,3,120310 +6359,3,120320 +6360,3,120330 +6361,3,120340 +6362,3,120350 +6363,3,120360 +6364,3,120370 +6365,3,120380 +6366,3,120390 +6367,3,120400 +6368,3,120410 +6369,3,120420 +6370,3,120430 +6371,3,120450 +6372,3,120460 +6373,3,120550 +6374,3,120560 +6375,3,120570 +6376,3,120990 +6377,3,121000 +6378,3,121010 +6379,3,121020 +6380,4,140000 +6381,4,150010 +6382,4,150020 +6383,4,150030 +6384,4,150040 +6386,3,106000060 +6387,3,106000070 +6388,3,106000080 +6389,3,101000070 +6390,3,110000060 +6391,3,104000060 +6392,3,104000070 +6393,3,104000080 +6394,3,102000050 +6395,3,104000170 +6396,3,104000260 +6397,4,106000100 +6398,4,106000110 +6399,4,104000090 +6400,4,104000100 +6401,4,104000110 +6402,4,107000090 +6403,4,104000180 +6404,5,106000120 +6405,5,104000120 +6406,5,104000190 +6407,1,103000000 +6408,2,103000001 +6409,3,103000002 +6410,4,103000003 +6411,5,103000004 +6412,1,111000000 +6413,2,111000001 +6414,3,111000002 +6415,4,111000003 +6416,5,111000004 +6417,1,115000000 +6418,2,115000001 +6419,3,115000002 +6420,4,115000003 +6421,5,115000004 +6422,4,130024 +6423,4,130034 +6424,4,130044 +6425,4,130054 +6426,3,130060 +6427,3,130070 +6428,3,130080 +6429,3,130090 +6430,3,130100 +6431,3,130110 +6432,3,130120 +6433,3,130130 +6434,3,130140 +6435,3,130150 +6436,3,130160 +6437,3,130170 +6438,3,130180 +6439,3,130190 +6440,3,130200 +6441,3,130420 +6442,3,130510 +6443,3,130520 +6444,3,130530 +6445,3,130540 +6446,3,130660 +6447,3,130790 +6448,3,130800 +6449,4,140000 +6450,4,150010 +6451,4,150020 +6452,4,150030 +6453,4,150040 +6455,1,101000010 +6456,1,102000010 +6457,1,103000010 +6458,1,104000010 +6459,1,105000010 +6460,1,106000010 +6461,1,107000010 +6462,1,108000010 +6463,1,109000010 +6464,1,110000010 +6465,2,101000020 +6466,2,101000030 +6467,2,102000020 +6468,2,102000030 +6469,2,102000040 +6470,2,103000020 +6471,2,103000030 +6472,2,103000040 +6473,2,104000020 +6474,2,104000030 +6475,2,104000040 +6476,2,105000020 +6477,2,105000030 +6478,2,105000040 +6479,2,106000020 +6480,2,106000030 +6481,2,106000040 +6482,2,107000020 +6483,2,107000030 +6484,2,107000040 +6485,2,108000020 +6486,2,108000030 +6487,2,108000040 +6488,2,109000020 +6489,2,109000030 +6490,2,109000040 +6491,2,110000020 +6492,2,110000030 +6493,2,110000040 +6494,2,118000010 +6495,3,101000050 +6496,3,101000060 +6497,3,101000080 +6498,3,101000040 +6499,3,109000060 +6500,3,109000070 +6501,3,109000080 +6502,3,105000060 +6503,3,105000070 +6504,3,105000080 +6505,3,104000050 +6506,3,106000050 +6507,3,102000060 +6508,3,102000070 +6509,3,102000080 +6510,3,103000050 +6511,3,105000050 +6512,3,107000060 +6513,3,107000070 +6514,3,107000080 +6515,3,108000050 +6516,3,109000050 +6517,3,103000060 +6518,3,103000070 +6519,3,103000080 +6520,3,110000050 +6521,3,106000060 +6522,3,106000070 +6523,3,106000080 +6524,3,101000070 +6525,3,110000060 +6526,3,104000060 +6527,3,104000070 +6528,3,104000080 +6529,3,102000050 +6530,3,104000170 +6531,3,104000260 +6532,3,111000010 +6533,3,111000020 +6534,3,111000030 +6535,3,112000020 +6536,3,112000030 +6537,3,108000060 +6538,3,108000070 +6539,3,108000080 +6540,3,107000050 +6541,3,112000010 +6542,3,110000070 +6543,3,110000080 +6544,3,118000020 +6545,3,118000030 +6546,3,118000040 +6547,4,101000090 +6548,4,101000100 +6549,4,101000110 +6550,4,109000100 +6551,4,105000100 +6552,4,105000110 +6553,4,108000090 +6554,4,110000090 +6555,4,102000100 +6556,4,102000110 +6557,4,106000090 +6558,4,109000090 +6559,4,107000100 +6560,4,103000090 +6561,4,102000090 +6562,4,103000100 +6563,4,106000100 +6564,4,106000110 +6565,4,104000090 +6566,4,104000100 +6567,4,104000110 +6568,4,107000090 +6569,4,104000180 +6570,4,111000040 +6571,4,112000040 +6572,4,108000100 +6573,4,105000090 +6574,4,110000100 +6575,4,118000050 +6576,4,118000060 +6577,5,101000120 +6578,5,109000110 +6579,5,105000120 +6580,5,102000120 +6581,5,107000110 +6582,5,103000120 +6583,5,106000120 +6584,5,104000120 +6585,5,104000190 +6586,5,111000060 +6587,5,112000060 +6588,5,108000110 +6589,5,110000110 +6590,5,118000070 +6591,1,170002 +6592,2,170003 +6593,3,170004 +6594,1,180002 +6595,2,180003 +6596,3,180004 +6597,1,201000010 +6598,1,292000010 +6599,1,299000040 +6600,1,299000070 +6601,1,299000110 +6602,1,299000120 +6603,1,299000140 +6604,2,202000010 +6605,2,290000010 +6606,2,299000010 +6607,2,299000150 +6608,2,299000190 +6609,2,299000200 +6610,2,299000210 +6611,3,298000050 +6612,3,298000060 +6613,3,299000060 +6614,3,299000170 +6615,5,297000100 +6616,5,291000020 +6617,4,140000 +6618,5,150010 +6619,5,150020 +6620,5,150030 +6621,5,150040 +6623,1,101000010 +6624,1,102000010 +6625,1,103000010 +6626,1,104000010 +6627,1,105000010 +6628,1,106000010 +6629,1,107000010 +6630,1,108000010 +6631,1,109000010 +6632,1,110000010 +6633,2,101000020 +6634,2,101000030 +6635,2,102000020 +6636,2,102000030 +6637,2,102000040 +6638,2,103000020 +6639,2,103000030 +6640,2,103000040 +6641,2,104000020 +6642,2,104000030 +6643,2,104000040 +6644,2,105000020 +6645,2,105000030 +6646,2,105000040 +6647,2,106000020 +6648,2,106000030 +6649,2,106000040 +6650,2,107000020 +6651,2,107000030 +6652,2,107000040 +6653,2,108000020 +6654,2,108000030 +6655,2,108000040 +6656,2,109000020 +6657,2,109000030 +6658,2,109000040 +6659,2,110000020 +6660,2,110000030 +6661,2,110000040 +6662,2,118000010 +6663,3,101000050 +6664,3,101000060 +6665,3,101000080 +6666,3,101000040 +6667,3,109000060 +6668,3,109000070 +6669,3,109000080 +6670,3,105000060 +6671,3,105000070 +6672,3,105000080 +6673,3,104000050 +6674,3,106000050 +6675,3,102000060 +6676,3,102000070 +6677,3,102000080 +6678,3,103000050 +6679,3,105000050 +6680,3,107000060 +6681,3,107000070 +6682,3,107000080 +6683,3,108000050 +6684,3,109000050 +6685,3,103000060 +6686,3,103000070 +6687,3,103000080 +6688,3,110000050 +6689,3,106000060 +6690,3,106000070 +6691,3,106000080 +6692,3,101000070 +6693,3,110000060 +6694,3,104000060 +6695,3,104000070 +6696,3,104000080 +6697,3,102000050 +6698,3,104000170 +6699,3,104000260 +6700,3,111000010 +6701,3,111000020 +6702,3,111000030 +6703,3,112000020 +6704,3,112000030 +6705,3,108000060 +6706,3,108000070 +6707,3,108000080 +6708,3,107000050 +6709,3,112000010 +6710,3,110000070 +6711,3,110000080 +6712,3,118000020 +6713,3,118000030 +6714,3,118000040 +6715,4,101000090 +6716,4,101000100 +6717,4,101000110 +6718,4,109000100 +6719,4,105000100 +6720,4,105000110 +6721,4,108000090 +6722,4,110000090 +6723,4,102000100 +6724,4,102000110 +6725,4,106000090 +6726,4,109000090 +6727,4,107000100 +6728,4,103000090 +6729,4,102000090 +6730,4,103000100 +6731,4,106000100 +6732,4,106000110 +6733,4,104000090 +6734,4,104000100 +6735,4,104000110 +6736,4,107000090 +6737,4,104000180 +6738,4,111000040 +6739,4,112000040 +6740,4,108000100 +6741,4,105000090 +6742,4,110000100 +6743,4,118000050 +6744,4,118000060 +6745,5,101000120 +6746,5,109000110 +6747,5,105000120 +6748,5,102000120 +6749,5,107000110 +6750,5,103000120 +6751,5,106000120 +6752,5,104000120 +6753,5,104000190 +6754,5,111000060 +6755,5,112000060 +6756,5,108000110 +6757,5,110000110 +6758,5,118000070 +6759,2,170003 +6760,3,170004 +6761,2,180003 +6762,3,180004 +6763,1,201000010 +6764,1,292000010 +6765,1,299000040 +6766,1,299000070 +6767,1,299000110 +6768,1,299000120 +6769,1,299000140 +6770,2,202000010 +6771,2,290000010 +6772,2,299000010 +6773,2,299000150 +6774,2,299000190 +6775,2,299000200 +6776,2,299000210 +6777,3,298000050 +6778,3,298000060 +6779,3,299000060 +6780,3,299000170 +6781,5,297000100 +6782,5,291000020 +6783,4,140000 +6784,5,150010 +6785,5,150020 +6786,5,150030 +6787,5,150040 +6789,3,101000050 +6790,3,101000060 +6791,3,101000080 +6792,3,101000040 +6793,3,109000060 +6794,3,109000070 +6795,3,109000080 +6796,3,105000060 +6797,3,105000070 +6798,3,105000080 +6799,3,104000050 +6800,3,106000050 +6801,3,102000060 +6802,3,102000070 +6803,3,102000080 +6804,3,103000050 +6805,3,105000050 +6806,3,107000060 +6807,3,107000070 +6808,3,107000080 +6809,3,108000050 +6810,3,109000050 +6811,3,103000060 +6812,3,103000070 +6813,3,103000080 +6814,3,110000050 +6815,3,106000060 +6816,3,106000070 +6817,3,106000080 +6818,3,101000070 +6819,3,110000060 +6820,3,104000060 +6821,3,104000070 +6822,3,104000080 +6823,3,102000050 +6824,3,104000170 +6825,3,104000260 +6826,3,111000010 +6827,3,111000020 +6828,3,111000030 +6829,3,112000020 +6830,3,112000030 +6831,3,108000060 +6832,3,108000070 +6833,3,108000080 +6834,3,107000050 +6835,3,112000010 +6836,3,110000070 +6837,3,110000080 +6838,3,118000020 +6839,3,118000030 +6840,3,118000040 +6841,4,101000090 +6842,4,101000100 +6843,4,101000110 +6844,4,109000100 +6845,4,105000100 +6846,4,105000110 +6847,4,108000090 +6848,4,110000090 +6849,4,102000100 +6850,4,102000110 +6851,4,106000090 +6852,4,109000090 +6853,4,107000100 +6854,4,103000090 +6855,4,102000090 +6856,4,103000100 +6857,4,106000100 +6858,4,106000110 +6859,4,104000090 +6860,4,104000100 +6861,4,104000110 +6862,4,107000090 +6863,4,104000180 +6864,4,111000040 +6865,4,112000040 +6866,4,108000100 +6867,4,105000090 +6868,4,110000100 +6869,4,118000050 +6870,4,118000060 +6871,5,101000120 +6872,5,109000110 +6873,5,105000120 +6874,5,102000120 +6875,5,107000110 +6876,5,103000120 +6877,5,106000120 +6878,5,104000120 +6879,5,104000190 +6880,5,111000060 +6881,5,112000060 +6882,5,108000110 +6883,5,110000110 +6884,5,118000070 +6885,3,170004 +6886,3,180004 +6887,1,201000010 +6888,1,292000010 +6889,1,299000040 +6890,1,299000070 +6891,1,299000110 +6892,1,299000120 +6893,1,299000140 +6894,2,202000010 +6895,2,290000010 +6896,2,299000010 +6897,2,299000150 +6898,2,299000190 +6899,2,299000200 +6900,2,299000210 +6901,3,298000050 +6902,3,298000060 +6903,3,299000060 +6904,3,299000170 +6905,5,297000100 +6906,5,291000020 +6907,4,140000 +6908,5,150010 +6909,5,150020 +6910,5,150030 +6911,5,150040 +6913,3,101000050 +6914,3,101000060 +6915,3,101000080 +6916,3,101000040 +6917,3,109000060 +6918,3,109000070 +6919,3,109000080 +6920,3,105000060 +6921,3,105000070 +6922,3,105000080 +6923,3,104000050 +6924,3,106000050 +6925,3,102000060 +6926,3,102000070 +6927,3,102000080 +6928,3,103000050 +6929,3,105000050 +6930,3,107000060 +6931,3,107000070 +6932,3,107000080 +6933,3,108000050 +6934,3,109000050 +6935,3,103000060 +6936,3,103000070 +6937,3,103000080 +6938,3,110000050 +6939,3,106000060 +6940,3,106000070 +6941,3,106000080 +6942,3,101000070 +6943,3,110000060 +6944,3,104000060 +6945,3,104000070 +6946,3,104000080 +6947,3,102000050 +6948,3,104000170 +6949,3,104000260 +6950,3,111000010 +6951,3,111000020 +6952,3,111000030 +6953,3,112000020 +6954,3,112000030 +6955,3,108000060 +6956,3,108000070 +6957,3,108000080 +6958,3,107000050 +6959,3,112000010 +6960,3,110000070 +6961,3,110000080 +6962,3,118000020 +6963,3,118000030 +6964,3,118000040 +6965,4,101000090 +6966,4,101000100 +6967,4,101000110 +6968,4,109000100 +6969,4,105000100 +6970,4,105000110 +6971,4,108000090 +6972,4,110000090 +6973,4,102000100 +6974,4,102000110 +6975,4,106000090 +6976,4,109000090 +6977,4,107000100 +6978,4,103000090 +6979,4,102000090 +6980,4,103000100 +6981,4,106000100 +6982,4,106000110 +6983,4,104000090 +6984,4,104000100 +6985,4,104000110 +6986,4,107000090 +6987,4,104000180 +6988,4,111000040 +6989,4,112000040 +6990,4,108000100 +6991,4,105000090 +6992,4,110000100 +6993,4,118000050 +6994,4,118000060 +6995,5,101000120 +6996,5,109000110 +6997,5,105000120 +6998,5,102000120 +6999,5,107000110 +7000,5,103000120 +7001,5,106000120 +7002,5,104000120 +7003,5,104000190 +7004,5,111000060 +7005,5,112000060 +7006,5,108000110 +7007,5,110000110 +7008,5,118000070 +7009,3,170004 +7010,3,180004 +7011,1,201000010 +7012,1,292000010 +7013,1,299000040 +7014,1,299000070 +7015,1,299000110 +7016,1,299000120 +7017,1,299000140 +7018,2,202000010 +7019,2,290000010 +7020,2,299000010 +7021,2,299000150 +7022,2,299000190 +7023,2,299000200 +7024,2,299000210 +7025,3,298000050 +7026,3,298000060 +7027,3,299000060 +7028,3,299000170 +7029,5,297000100 +7030,5,291000020 +7031,4,140000 +7032,5,150010 +7033,5,150020 +7034,5,150030 +7035,5,150040 +7037,3,101000050 +7038,3,101000060 +7039,3,101000080 +7040,3,101000040 +7041,3,109000060 +7042,3,109000070 +7043,3,109000080 +7044,3,105000060 +7045,3,105000070 +7046,3,105000080 +7047,3,104000050 +7048,3,106000050 +7049,3,102000060 +7050,3,102000070 +7051,3,102000080 +7052,3,103000050 +7053,3,105000050 +7054,3,107000060 +7055,3,107000070 +7056,3,107000080 +7057,3,108000050 +7058,3,109000050 +7059,3,103000060 +7060,3,103000070 +7061,3,103000080 +7062,3,110000050 +7063,3,106000060 +7064,3,106000070 +7065,3,106000080 +7066,3,101000070 +7067,3,110000060 +7068,3,104000060 +7069,3,104000070 +7070,3,104000080 +7071,3,102000050 +7072,3,104000170 +7073,3,104000260 +7074,3,111000010 +7075,3,111000020 +7076,3,111000030 +7077,3,112000020 +7078,3,112000030 +7079,3,108000060 +7080,3,108000070 +7081,3,108000080 +7082,3,107000050 +7083,3,112000010 +7084,3,110000070 +7085,3,110000080 +7086,3,118000020 +7087,3,118000030 +7088,3,118000040 +7089,4,101000090 +7090,4,101000100 +7091,4,101000110 +7092,4,109000100 +7093,4,105000100 +7094,4,105000110 +7095,4,108000090 +7096,4,110000090 +7097,4,102000100 +7098,4,102000110 +7099,4,106000090 +7100,4,109000090 +7101,4,107000100 +7102,4,103000090 +7103,4,102000090 +7104,4,103000100 +7105,4,106000100 +7106,4,106000110 +7107,4,104000090 +7108,4,104000100 +7109,4,104000110 +7110,4,107000090 +7111,4,104000180 +7112,4,111000040 +7113,4,112000040 +7114,4,108000100 +7115,4,105000090 +7116,4,110000100 +7117,4,118000050 +7118,4,118000060 +7119,5,101000120 +7120,5,109000110 +7121,5,105000120 +7122,5,102000120 +7123,5,107000110 +7124,5,103000120 +7125,5,106000120 +7126,5,104000120 +7127,5,104000190 +7128,5,111000060 +7129,5,112000060 +7130,5,108000110 +7131,5,110000110 +7132,5,118000070 +7133,3,170004 +7134,3,180004 +7135,1,201000010 +7136,1,292000010 +7137,1,299000040 +7138,1,299000070 +7139,1,299000110 +7140,1,299000120 +7141,1,299000140 +7142,2,202000010 +7143,2,290000010 +7144,2,299000010 +7145,2,299000150 +7146,2,299000190 +7147,2,299000200 +7148,2,299000210 +7149,3,298000050 +7150,3,298000060 +7151,3,299000060 +7152,3,299000170 +7153,5,297000100 +7154,5,291000020 +7155,4,140000 +7156,5,150010 +7157,5,150020 +7158,5,150030 +7159,5,150040 +7160,5,190000 +7161,5,200000 +7162,5,210000 +7164,3,118000020 +7165,3,118000030 +7166,3,118000040 +7167,3,106000060 +7168,3,106000070 +7169,3,106000080 +7170,3,101000070 +7171,3,110000060 +7172,3,104000060 +7173,3,104000070 +7174,3,104000080 +7175,3,102000050 +7176,3,104000170 +7177,3,104000260 +7178,4,118000050 +7179,4,118000060 +7180,4,106000100 +7181,4,106000110 +7182,4,104000090 +7183,4,104000100 +7184,4,104000110 +7185,4,104000270 +7186,4,107000090 +7187,4,104000180 +7188,5,118000070 +7189,5,106000120 +7190,5,104000120 +7191,5,104000190 +7192,1,103000000 +7193,2,103000001 +7194,3,103000002 +7195,4,103000003 +7196,5,103000004 +7197,1,111000000 +7198,2,111000001 +7199,3,111000002 +7200,4,111000003 +7201,5,111000004 +7202,1,115000000 +7203,2,115000001 +7204,3,115000002 +7205,4,115000003 +7206,5,115000004 +7207,3,170004 +7208,4,170005 +7209,3,180004 +7210,4,180005 +7211,4,140000 +7212,4,150010 +7213,4,150020 +7214,4,150030 +7215,4,150040 +7217,3,111000010 +7218,3,111000020 +7219,3,111000030 +7220,3,112000020 +7221,3,112000030 +7222,3,108000060 +7223,3,108000070 +7224,3,108000080 +7225,3,107000050 +7226,3,112000010 +7227,3,110000070 +7228,3,110000080 +7229,4,111000040 +7230,4,112000040 +7231,4,108000100 +7232,4,105000090 +7233,4,110000100 +7234,5,111000060 +7235,5,112000060 +7236,5,108000110 +7237,5,110000110 +7238,1,108000000 +7239,2,108000001 +7240,3,108000002 +7241,4,108000003 +7242,5,108000004 +7243,1,107000000 +7244,2,107000001 +7245,3,107000002 +7246,4,107000003 +7247,5,107000004 +7248,1,120000000 +7249,2,120000001 +7250,3,120000002 +7251,4,120000003 +7252,5,120000004 +7253,4,110024 +7254,4,110034 +7255,4,110044 +7256,4,110054 +7257,3,110060 +7258,3,110070 +7259,3,110080 +7260,3,110090 +7261,3,110100 +7262,3,110110 +7263,3,110120 +7264,3,110130 +7265,3,110140 +7266,3,110150 +7267,3,110160 +7268,3,110170 +7269,3,110180 +7270,3,110190 +7271,3,110200 +7272,3,110210 +7273,3,110220 +7274,3,110230 +7275,3,110240 +7276,3,110250 +7277,3,110260 +7278,3,110270 +7279,3,110620 +7280,3,110670 +7281,4,140000 +7282,4,150010 +7283,4,150020 +7284,4,150030 +7285,4,150040 +7287,3,101000050 +7288,3,101000060 +7289,3,101000080 +7290,3,101000040 +7291,3,109000060 +7292,3,109000070 +7293,3,109000080 +7294,3,105000060 +7295,3,105000070 +7296,3,105000080 +7297,3,104000050 +7298,3,106000050 +7299,4,101000090 +7300,4,101000100 +7301,4,101000110 +7302,4,109000100 +7303,4,105000100 +7304,4,105000110 +7305,4,108000090 +7306,4,110000090 +7307,5,101000120 +7308,5,109000110 +7309,5,105000120 +7310,1,101000000 +7311,2,101000001 +7312,3,101000002 +7313,4,101000008 +7314,5,101000004 +7315,1,109000000 +7316,2,109000001 +7317,3,109000002 +7318,4,109000003 +7319,5,109000004 +7320,4,120024 +7321,4,120034 +7322,4,120044 +7323,4,120054 +7324,3,120241 +7325,3,120251 +7326,3,120261 +7327,3,120271 +7328,3,120300 +7329,3,120310 +7330,3,120320 +7331,3,120330 +7332,3,120340 +7333,3,120350 +7334,3,120360 +7335,3,120370 +7336,3,120380 +7337,3,120390 +7338,3,120400 +7339,3,120410 +7340,3,120420 +7341,3,120430 +7342,3,120450 +7343,3,120460 +7344,3,120550 +7345,3,120560 +7346,3,120570 +7347,3,120990 +7348,3,121000 +7349,3,121010 +7350,3,121020 +7351,4,140000 +7352,4,150010 +7353,4,150020 +7354,4,150030 +7355,4,150040 +7357,3,102000060 +7358,3,102000070 +7359,3,102000080 +7360,3,103000050 +7361,3,105000050 +7362,3,107000060 +7363,3,107000070 +7364,3,107000080 +7365,3,108000050 +7366,3,109000050 +7367,3,103000060 +7368,3,103000070 +7369,3,103000080 +7370,3,110000050 +7371,4,102000100 +7372,4,102000110 +7373,4,106000090 +7374,4,109000090 +7375,4,107000100 +7376,4,103000090 +7377,4,102000090 +7378,4,103000100 +7379,5,102000120 +7380,5,107000110 +7381,5,103000120 +7382,1,102000000 +7383,2,102000001 +7384,3,102000002 +7385,4,102000003 +7386,5,102000004 +7387,1,105000000 +7388,2,105000001 +7389,3,105000002 +7390,4,105000003 +7391,5,105000004 +7392,1,112000000 +7393,2,112000001 +7394,3,112000002 +7395,4,112000003 +7396,5,112000004 +7397,4,130024 +7398,4,130034 +7399,4,130044 +7400,4,130054 +7401,3,130060 +7402,3,130070 +7403,3,130080 +7404,3,130090 +7405,3,130100 +7406,3,130110 +7407,3,130120 +7408,3,130130 +7409,3,130140 +7410,3,130150 +7411,3,130160 +7412,3,130170 +7413,3,130180 +7414,3,130190 +7415,3,130200 +7416,3,130420 +7417,3,130510 +7418,3,130520 +7419,3,130530 +7420,3,130540 +7421,3,130660 +7422,3,130790 +7423,3,130800 +7424,4,140000 +7425,4,150010 +7426,4,150020 +7427,4,150030 +7428,4,150040 +7430,1,101000010 +7431,1,102000010 +7432,1,103000010 +7433,1,104000010 +7434,1,105000010 +7435,1,106000010 +7436,1,107000010 +7437,1,108000010 +7438,1,109000010 +7439,1,110000010 +7440,2,101000020 +7441,2,101000030 +7442,2,102000020 +7443,2,102000030 +7444,2,102000040 +7445,2,103000020 +7446,2,103000030 +7447,2,103000040 +7448,2,104000020 +7449,2,104000030 +7450,2,104000040 +7451,2,105000020 +7452,2,105000030 +7453,2,105000040 +7454,2,106000020 +7455,2,106000030 +7456,2,106000040 +7457,2,107000020 +7458,2,107000030 +7459,2,107000040 +7460,2,108000020 +7461,2,108000030 +7462,2,108000040 +7463,2,109000020 +7464,2,109000030 +7465,2,109000040 +7466,2,110000020 +7467,2,110000030 +7468,2,110000040 +7469,2,118000010 +7470,3,101000050 +7471,3,101000060 +7472,3,101000080 +7473,3,101000040 +7474,3,109000060 +7475,3,109000070 +7476,3,109000080 +7477,3,105000060 +7478,3,105000070 +7479,3,105000080 +7480,3,104000050 +7481,3,106000050 +7482,3,102000060 +7483,3,102000070 +7484,3,102000080 +7485,3,103000050 +7486,3,105000050 +7487,3,107000060 +7488,3,107000070 +7489,3,107000080 +7490,3,108000050 +7491,3,109000050 +7492,3,103000060 +7493,3,103000070 +7494,3,103000080 +7495,3,110000050 +7496,3,106000060 +7497,3,106000070 +7498,3,106000080 +7499,3,101000070 +7500,3,110000060 +7501,3,104000060 +7502,3,104000070 +7503,3,104000080 +7504,3,102000050 +7505,3,104000170 +7506,3,104000260 +7507,3,111000010 +7508,3,111000020 +7509,3,111000030 +7510,3,112000020 +7511,3,112000030 +7512,3,108000060 +7513,3,108000070 +7514,3,108000080 +7515,3,107000050 +7516,3,112000010 +7517,3,110000070 +7518,3,110000080 +7519,3,118000020 +7520,3,118000030 +7521,3,118000040 +7522,4,101000090 +7523,4,101000100 +7524,4,101000110 +7525,4,109000100 +7526,4,105000100 +7527,4,105000110 +7528,4,108000090 +7529,4,110000090 +7530,4,102000100 +7531,4,102000110 +7532,4,106000090 +7533,4,109000090 +7534,4,107000100 +7535,4,103000090 +7536,4,102000090 +7537,4,103000100 +7538,4,106000100 +7539,4,106000110 +7540,4,104000090 +7541,4,104000100 +7542,4,104000110 +7543,4,107000090 +7544,4,104000180 +7545,4,111000040 +7546,4,112000040 +7547,4,108000100 +7548,4,105000090 +7549,4,110000100 +7550,4,118000050 +7551,4,118000060 +7552,5,101000120 +7553,5,109000110 +7554,5,105000120 +7555,5,102000120 +7556,5,107000110 +7557,5,103000120 +7558,5,106000120 +7559,5,104000120 +7560,5,104000190 +7561,5,111000060 +7562,5,112000060 +7563,5,108000110 +7564,5,110000110 +7565,5,118000070 +7566,1,170002 +7567,2,170003 +7568,3,170004 +7569,1,180002 +7570,2,180003 +7571,3,180004 +7572,1,201000010 +7573,1,292000010 +7574,1,299000040 +7575,1,299000070 +7576,1,299000110 +7577,1,299000120 +7578,1,299000140 +7579,2,202000010 +7580,2,290000010 +7581,2,299000010 +7582,2,299000150 +7583,2,299000190 +7584,2,299000200 +7585,2,299000210 +7586,3,298000050 +7587,3,298000060 +7588,3,299000060 +7589,3,299000170 +7590,3,290000120 +7591,3,291000050 +7592,3,292000020 +7593,4,299000670 +7594,4,299000680 +7595,4,204000010 +7596,5,297000100 +7597,5,291000020 +7598,5,297000130 +7599,5,297000140 +7600,5,203000010 +7601,4,140000 +7602,5,150010 +7603,5,150020 +7604,5,150030 +7605,5,150040 +7607,1,101000010 +7608,1,102000010 +7609,1,103000010 +7610,1,104000010 +7611,1,105000010 +7612,1,106000010 +7613,1,107000010 +7614,1,108000010 +7615,1,109000010 +7616,1,110000010 +7617,2,101000020 +7618,2,101000030 +7619,2,102000020 +7620,2,102000030 +7621,2,102000040 +7622,2,103000020 +7623,2,103000030 +7624,2,103000040 +7625,2,104000020 +7626,2,104000030 +7627,2,104000040 +7628,2,105000020 +7629,2,105000030 +7630,2,105000040 +7631,2,106000020 +7632,2,106000030 +7633,2,106000040 +7634,2,107000020 +7635,2,107000030 +7636,2,107000040 +7637,2,108000020 +7638,2,108000030 +7639,2,108000040 +7640,2,109000020 +7641,2,109000030 +7642,2,109000040 +7643,2,110000020 +7644,2,110000030 +7645,2,110000040 +7646,2,118000010 +7647,3,101000050 +7648,3,101000060 +7649,3,101000080 +7650,3,101000040 +7651,3,109000060 +7652,3,109000070 +7653,3,109000080 +7654,3,105000060 +7655,3,105000070 +7656,3,105000080 +7657,3,104000050 +7658,3,106000050 +7659,3,102000060 +7660,3,102000070 +7661,3,102000080 +7662,3,103000050 +7663,3,105000050 +7664,3,107000060 +7665,3,107000070 +7666,3,107000080 +7667,3,108000050 +7668,3,109000050 +7669,3,103000060 +7670,3,103000070 +7671,3,103000080 +7672,3,110000050 +7673,3,106000060 +7674,3,106000070 +7675,3,106000080 +7676,3,101000070 +7677,3,110000060 +7678,3,104000060 +7679,3,104000070 +7680,3,104000080 +7681,3,102000050 +7682,3,104000170 +7683,3,104000260 +7684,3,111000010 +7685,3,111000020 +7686,3,111000030 +7687,3,112000020 +7688,3,112000030 +7689,3,108000060 +7690,3,108000070 +7691,3,108000080 +7692,3,107000050 +7693,3,112000010 +7694,3,110000070 +7695,3,110000080 +7696,3,118000020 +7697,3,118000030 +7698,3,118000040 +7699,4,101000090 +7700,4,101000100 +7701,4,101000110 +7702,4,109000100 +7703,4,105000100 +7704,4,105000110 +7705,4,108000090 +7706,4,110000090 +7707,4,102000100 +7708,4,102000110 +7709,4,106000090 +7710,4,109000090 +7711,4,107000100 +7712,4,103000090 +7713,4,102000090 +7714,4,103000100 +7715,4,106000100 +7716,4,106000110 +7717,4,104000090 +7718,4,104000100 +7719,4,104000110 +7720,4,107000090 +7721,4,104000180 +7722,4,111000040 +7723,4,112000040 +7724,4,108000100 +7725,4,105000090 +7726,4,110000100 +7727,4,118000050 +7728,4,118000060 +7729,5,101000120 +7730,5,109000110 +7731,5,105000120 +7732,5,102000120 +7733,5,107000110 +7734,5,103000120 +7735,5,106000120 +7736,5,104000120 +7737,5,104000190 +7738,5,111000060 +7739,5,112000060 +7740,5,108000110 +7741,5,110000110 +7742,5,118000070 +7743,2,170003 +7744,3,170004 +7745,2,180003 +7746,3,180004 +7747,1,201000010 +7748,1,292000010 +7749,1,299000040 +7750,1,299000070 +7751,1,299000110 +7752,1,299000120 +7753,1,299000140 +7754,2,202000010 +7755,2,290000010 +7756,2,299000010 +7757,2,299000150 +7758,2,299000190 +7759,2,299000200 +7760,2,299000210 +7761,3,298000050 +7762,3,298000060 +7763,3,299000060 +7764,3,299000170 +7765,3,290000120 +7766,3,291000050 +7767,3,292000020 +7768,4,299000670 +7769,4,299000680 +7770,4,204000010 +7771,5,297000100 +7772,5,291000020 +7773,5,297000130 +7774,5,297000140 +7775,5,203000010 +7776,4,140000 +7777,5,150010 +7778,5,150020 +7779,5,150030 +7780,5,150040 +7782,3,101000050 +7783,3,101000060 +7784,3,101000080 +7785,3,101000040 +7786,3,109000060 +7787,3,109000070 +7788,3,109000080 +7789,3,105000060 +7790,3,105000070 +7791,3,105000080 +7792,3,104000050 +7793,3,106000050 +7794,3,102000060 +7795,3,102000070 +7796,3,102000080 +7797,3,103000050 +7798,3,105000050 +7799,3,107000060 +7800,3,107000070 +7801,3,107000080 +7802,3,108000050 +7803,3,109000050 +7804,3,103000060 +7805,3,103000070 +7806,3,103000080 +7807,3,110000050 +7808,3,106000060 +7809,3,106000070 +7810,3,106000080 +7811,3,101000070 +7812,3,110000060 +7813,3,104000060 +7814,3,104000070 +7815,3,104000080 +7816,3,102000050 +7817,3,104000170 +7818,3,104000260 +7819,3,111000010 +7820,3,111000020 +7821,3,111000030 +7822,3,112000020 +7823,3,112000030 +7824,3,108000060 +7825,3,108000070 +7826,3,108000080 +7827,3,107000050 +7828,3,112000010 +7829,3,110000070 +7830,3,110000080 +7831,3,118000020 +7832,3,118000030 +7833,3,118000040 +7834,4,101000090 +7835,4,101000100 +7836,4,101000110 +7837,4,109000100 +7838,4,105000100 +7839,4,105000110 +7840,4,108000090 +7841,4,110000090 +7842,4,102000100 +7843,4,102000110 +7844,4,106000090 +7845,4,109000090 +7846,4,107000100 +7847,4,103000090 +7848,4,102000090 +7849,4,103000100 +7850,4,106000100 +7851,4,106000110 +7852,4,104000090 +7853,4,104000100 +7854,4,104000110 +7855,4,107000090 +7856,4,104000180 +7857,4,111000040 +7858,4,112000040 +7859,4,108000100 +7860,4,105000090 +7861,4,110000100 +7862,4,118000050 +7863,4,118000060 +7864,5,101000120 +7865,5,109000110 +7866,5,105000120 +7867,5,102000120 +7868,5,107000110 +7869,5,103000120 +7870,5,106000120 +7871,5,104000120 +7872,5,104000190 +7873,5,111000060 +7874,5,112000060 +7875,5,108000110 +7876,5,110000110 +7877,5,118000070 +7878,3,170004 +7879,3,180004 +7880,1,201000010 +7881,1,292000010 +7882,1,299000040 +7883,1,299000070 +7884,1,299000110 +7885,1,299000120 +7886,1,299000140 +7887,2,202000010 +7888,2,290000010 +7889,2,299000010 +7890,2,299000150 +7891,2,299000190 +7892,2,299000200 +7893,2,299000210 +7894,3,298000050 +7895,3,298000060 +7896,3,299000060 +7897,3,299000170 +7898,3,290000120 +7899,3,291000050 +7900,3,292000020 +7901,4,299000670 +7902,4,299000680 +7903,4,204000010 +7904,5,297000100 +7905,5,291000020 +7906,5,297000130 +7907,5,297000140 +7908,5,203000010 +7909,4,140000 +7910,5,150010 +7911,5,150020 +7912,5,150030 +7913,5,150040 +7915,3,101000050 +7916,3,101000060 +7917,3,101000080 +7918,3,101000040 +7919,3,109000060 +7920,3,109000070 +7921,3,109000080 +7922,3,105000060 +7923,3,105000070 +7924,3,105000080 +7925,3,104000050 +7926,3,106000050 +7927,3,102000060 +7928,3,102000070 +7929,3,102000080 +7930,3,103000050 +7931,3,105000050 +7932,3,107000060 +7933,3,107000070 +7934,3,107000080 +7935,3,108000050 +7936,3,109000050 +7937,3,103000060 +7938,3,103000070 +7939,3,103000080 +7940,3,110000050 +7941,3,106000060 +7942,3,106000070 +7943,3,106000080 +7944,3,101000070 +7945,3,110000060 +7946,3,104000060 +7947,3,104000070 +7948,3,104000080 +7949,3,102000050 +7950,3,104000170 +7951,3,104000260 +7952,3,111000010 +7953,3,111000020 +7954,3,111000030 +7955,3,112000020 +7956,3,112000030 +7957,3,108000060 +7958,3,108000070 +7959,3,108000080 +7960,3,107000050 +7961,3,112000010 +7962,3,110000070 +7963,3,110000080 +7964,3,118000020 +7965,3,118000030 +7966,3,118000040 +7967,4,101000090 +7968,4,101000100 +7969,4,101000110 +7970,4,109000100 +7971,4,105000100 +7972,4,105000110 +7973,4,108000090 +7974,4,110000090 +7975,4,102000100 +7976,4,102000110 +7977,4,106000090 +7978,4,109000090 +7979,4,107000100 +7980,4,103000090 +7981,4,102000090 +7982,4,103000100 +7983,4,106000100 +7984,4,106000110 +7985,4,104000090 +7986,4,104000100 +7987,4,104000110 +7988,4,107000090 +7989,4,104000180 +7990,4,111000040 +7991,4,112000040 +7992,4,108000100 +7993,4,105000090 +7994,4,110000100 +7995,4,118000050 +7996,4,118000060 +7997,5,101000120 +7998,5,109000110 +7999,5,105000120 +8000,5,102000120 +8001,5,107000110 +8002,5,103000120 +8003,5,106000120 +8004,5,104000120 +8005,5,104000190 +8006,5,111000060 +8007,5,112000060 +8008,5,108000110 +8009,5,110000110 +8010,5,118000070 +8011,3,170004 +8012,3,180004 +8013,1,201000010 +8014,1,292000010 +8015,1,299000040 +8016,1,299000070 +8017,1,299000110 +8018,1,299000120 +8019,1,299000140 +8020,2,202000010 +8021,2,290000010 +8022,2,299000010 +8023,2,299000150 +8024,2,299000190 +8025,2,299000200 +8026,2,299000210 +8027,3,298000050 +8028,3,298000060 +8029,3,299000060 +8030,3,299000170 +8031,3,290000120 +8032,3,291000050 +8033,3,292000020 +8034,4,299000670 +8035,4,299000680 +8036,4,204000010 +8037,5,297000100 +8038,5,291000020 +8039,5,297000130 +8040,5,297000140 +8041,5,203000010 +8042,4,140000 +8043,5,150010 +8044,5,150020 +8045,5,150030 +8046,5,150040 +8048,3,101000050 +8049,3,101000060 +8050,3,101000080 +8051,3,101000040 +8052,3,109000060 +8053,3,109000070 +8054,3,109000080 +8055,3,105000060 +8056,3,105000070 +8057,3,105000080 +8058,3,104000050 +8059,3,106000050 +8060,3,102000060 +8061,3,102000070 +8062,3,102000080 +8063,3,103000050 +8064,3,105000050 +8065,3,107000060 +8066,3,107000070 +8067,3,107000080 +8068,3,108000050 +8069,3,109000050 +8070,3,103000060 +8071,3,103000070 +8072,3,103000080 +8073,3,110000050 +8074,3,106000060 +8075,3,106000070 +8076,3,106000080 +8077,3,101000070 +8078,3,110000060 +8079,3,104000060 +8080,3,104000070 +8081,3,104000080 +8082,3,102000050 +8083,3,104000170 +8084,3,104000260 +8085,3,111000010 +8086,3,111000020 +8087,3,111000030 +8088,3,112000020 +8089,3,112000030 +8090,3,108000060 +8091,3,108000070 +8092,3,108000080 +8093,3,107000050 +8094,3,112000010 +8095,3,110000070 +8096,3,110000080 +8097,3,118000020 +8098,3,118000030 +8099,3,118000040 +8100,4,101000090 +8101,4,101000100 +8102,4,101000110 +8103,4,109000100 +8104,4,105000100 +8105,4,105000110 +8106,4,108000090 +8107,4,110000090 +8108,4,102000100 +8109,4,102000110 +8110,4,106000090 +8111,4,109000090 +8112,4,107000100 +8113,4,103000090 +8114,4,102000090 +8115,4,103000100 +8116,4,106000100 +8117,4,106000110 +8118,4,104000090 +8119,4,104000100 +8120,4,104000110 +8121,4,107000090 +8122,4,104000180 +8123,4,111000040 +8124,4,112000040 +8125,4,108000100 +8126,4,105000090 +8127,4,110000100 +8128,4,118000050 +8129,4,118000060 +8130,5,101000120 +8131,5,109000110 +8132,5,105000120 +8133,5,102000120 +8134,5,107000110 +8135,5,103000120 +8136,5,106000120 +8137,5,104000120 +8138,5,104000190 +8139,5,111000060 +8140,5,112000060 +8141,5,108000110 +8142,5,110000110 +8143,5,118000070 +8144,3,170004 +8145,3,180004 +8146,1,201000010 +8147,1,292000010 +8148,1,299000040 +8149,1,299000070 +8150,1,299000110 +8151,1,299000120 +8152,1,299000140 +8153,2,202000010 +8154,2,290000010 +8155,2,299000010 +8156,2,299000150 +8157,2,299000190 +8158,2,299000200 +8159,2,299000210 +8160,3,298000050 +8161,3,298000060 +8162,3,299000060 +8163,3,299000170 +8164,3,290000120 +8165,3,291000050 +8166,3,292000020 +8167,4,299000670 +8168,4,299000680 +8169,4,204000010 +8170,5,297000100 +8171,5,291000020 +8172,5,297000130 +8173,5,297000140 +8174,5,203000010 +8175,4,140000 +8176,5,150010 +8177,5,150020 +8178,5,150030 +8179,5,150040 +8180,5,190000 +8181,5,200000 +8182,5,210000 +8184,3,102000060 +8185,3,102000070 +8186,3,102000080 +8187,3,103000050 +8188,3,105000050 +8189,3,107000060 +8190,3,107000070 +8191,3,107000080 +8192,3,108000050 +8193,3,109000050 +8194,3,103000060 +8195,3,103000070 +8196,3,103000080 +8197,3,110000050 +8198,4,102000100 +8199,4,102000110 +8200,4,106000090 +8201,4,109000090 +8202,4,107000100 +8203,4,103000090 +8204,4,102000090 +8205,4,103000100 +8206,5,102000120 +8207,5,107000110 +8208,5,103000120 +8209,1,102000000 +8210,2,102000001 +8211,3,102000002 +8212,4,102000003 +8213,5,102000004 +8214,1,105000000 +8215,2,105000001 +8216,3,105000002 +8217,4,105000003 +8218,5,105000004 +8219,1,112000000 +8220,2,112000001 +8221,3,112000002 +8222,4,112000003 +8223,5,112000004 +8224,3,170004 +8225,4,170005 +8226,3,180004 +8227,4,180005 +8228,4,140000 +8229,4,150010 +8230,4,150020 +8231,4,150030 +8232,4,150040 +8234,3,118000020 +8235,3,118000030 +8236,3,118000040 +8237,3,106000060 +8238,3,106000070 +8239,3,106000080 +8240,3,101000070 +8241,3,110000060 +8242,3,104000060 +8243,3,104000070 +8244,3,104000080 +8245,3,102000050 +8246,3,104000170 +8247,3,104000260 +8248,4,118000050 +8249,4,118000060 +8250,4,106000100 +8251,4,106000110 +8252,4,104000090 +8253,4,104000100 +8254,4,104000110 +8255,4,104000270 +8256,4,107000090 +8257,4,104000180 +8258,5,118000070 +8259,5,106000120 +8260,5,104000120 +8261,5,104000190 +8262,1,103000000 +8263,2,103000001 +8264,3,103000002 +8265,4,103000003 +8266,5,103000004 +8267,1,111000000 +8268,2,111000001 +8269,3,111000002 +8270,4,111000003 +8271,5,111000004 +8272,1,115000000 +8273,2,115000001 +8274,3,115000002 +8275,4,115000003 +8276,5,115000004 +8277,4,110024 +8278,4,110034 +8279,4,110044 +8280,4,110054 +8281,3,110060 +8282,3,110070 +8283,3,110080 +8284,3,110090 +8285,3,110100 +8286,3,110110 +8287,3,110120 +8288,3,110130 +8289,3,110140 +8290,3,110150 +8291,3,110160 +8292,3,110170 +8293,3,110180 +8294,3,110190 +8295,3,110200 +8296,3,110210 +8297,3,110220 +8298,3,110230 +8299,3,110240 +8300,3,110250 +8301,3,110260 +8302,3,110270 +8303,3,110620 +8304,3,110670 +8305,4,140000 +8306,4,150010 +8307,4,150020 +8308,4,150030 +8309,4,150040 +8311,3,111000010 +8312,3,111000020 +8313,3,111000030 +8314,3,112000020 +8315,3,112000030 +8316,3,108000060 +8317,3,108000070 +8318,3,108000080 +8319,3,107000050 +8320,3,112000010 +8321,3,110000070 +8322,3,110000080 +8323,4,111000040 +8324,4,112000040 +8325,4,108000100 +8326,4,105000090 +8327,4,110000100 +8328,5,111000060 +8329,5,112000060 +8330,5,108000110 +8331,5,110000110 +8332,1,108000000 +8333,2,108000001 +8334,3,108000002 +8335,4,108000003 +8336,5,108000004 +8337,1,107000000 +8338,2,107000001 +8339,3,107000002 +8340,4,107000003 +8341,5,107000004 +8342,1,120000000 +8343,2,120000001 +8344,3,120000002 +8345,4,120000003 +8346,5,120000004 +8347,4,120024 +8348,4,120034 +8349,4,120044 +8350,4,120054 +8351,3,120241 +8352,3,120251 +8353,3,120261 +8354,3,120271 +8355,3,120300 +8356,3,120310 +8357,3,120320 +8358,3,120330 +8359,3,120340 +8360,3,120350 +8361,3,120360 +8362,3,120370 +8363,3,120380 +8364,3,120390 +8365,3,120400 +8366,3,120410 +8367,3,120420 +8368,3,120430 +8369,3,120450 +8370,3,120460 +8371,3,120550 +8372,3,120560 +8373,3,120570 +8374,3,120990 +8375,3,121000 +8376,3,121010 +8377,3,121020 +8378,4,140000 +8379,4,150010 +8380,4,150020 +8381,4,150030 +8382,4,150040 +8384,3,101000050 +8385,3,101000060 +8386,3,101000080 +8387,3,101000040 +8388,3,109000060 +8389,3,109000070 +8390,3,109000080 +8391,3,105000060 +8392,3,105000070 +8393,3,105000080 +8394,3,104000050 +8395,3,106000050 +8396,4,101000090 +8397,4,101000100 +8398,4,101000110 +8399,4,109000100 +8400,4,105000100 +8401,4,105000110 +8402,4,108000090 +8403,4,110000090 +8404,5,101000120 +8405,5,109000110 +8406,5,105000120 +8407,1,101000000 +8408,2,101000001 +8409,3,101000002 +8410,4,101000008 +8411,5,101000004 +8412,1,109000000 +8413,2,109000001 +8414,3,109000002 +8415,4,109000003 +8416,5,109000004 +8417,4,130024 +8418,4,130034 +8419,4,130044 +8420,4,130054 +8421,3,130060 +8422,3,130070 +8423,3,130080 +8424,3,130090 +8425,3,130100 +8426,3,130110 +8427,3,130120 +8428,3,130130 +8429,3,130140 +8430,3,130150 +8431,3,130160 +8432,3,130170 +8433,3,130180 +8434,3,130190 +8435,3,130200 +8436,3,130420 +8437,3,130510 +8438,3,130520 +8439,3,130531 +8440,3,130540 +8441,3,130660 +8442,3,130790 +8443,3,130800 +8444,3,131130 +8445,4,140000 +8446,4,150010 +8447,4,150020 +8448,4,150030 +8449,4,150040 +8451,1,101000000 +8452,2,101000001 +8453,3,101000002 +8454,4,101000008 +8455,5,101000004 +8456,1,102000000 +8457,2,102000001 +8458,3,102000002 +8459,4,102000003 +8460,5,102000004 +8461,1,103000000 +8462,2,103000001 +8463,3,103000002 +8464,4,103000003 +8465,5,103000004 +8466,1,105000000 +8467,2,105000001 +8468,3,105000002 +8469,4,105000003 +8470,5,105000004 +8471,1,108000000 +8472,2,108000001 +8473,3,108000002 +8474,4,108000003 +8475,5,108000004 +8476,1,109000000 +8477,2,109000001 +8478,3,109000002 +8479,4,109000003 +8480,5,109000004 +8481,1,111000000 +8482,2,111000001 +8483,3,111000002 +8484,4,111000003 +8485,5,111000004 +8486,1,112000000 +8487,2,112000001 +8488,3,112000002 +8489,4,112000003 +8490,5,112000004 +8491,1,120000000 +8492,2,120000001 +8493,3,120000002 +8494,4,120000003 +8495,5,120000004 +8496,1,101000010 +8497,2,101000020 +8498,2,101000030 +8499,3,101000040 +8500,3,101000050 +8501,3,101000060 +8502,3,101000070 +8503,3,101000080 +8504,1,102000010 +8505,2,102000020 +8506,2,102000030 +8507,2,102000040 +8508,3,102000050 +8509,3,102000060 +8510,3,102000070 +8511,3,102000080 +8512,1,103000010 +8513,2,103000020 +8514,2,103000030 +8515,2,103000040 +8516,3,103000050 +8517,3,103000060 +8518,3,103000070 +8519,3,103000080 +8520,1,104000010 +8521,2,104000020 +8522,2,104000030 +8523,2,104000040 +8524,3,104000050 +8525,3,104000060 +8526,3,104000070 +8527,3,104000080 +8528,1,105000010 +8529,2,105000020 +8530,2,105000030 +8531,2,105000040 +8532,3,105000050 +8533,3,105000060 +8534,3,105000070 +8535,3,105000080 +8536,1,106000010 +8537,2,106000020 +8538,2,106000030 +8539,2,106000040 +8540,3,106000050 +8541,3,106000060 +8542,3,106000070 +8543,3,106000080 +8544,1,107000010 +8545,2,107000020 +8546,2,107000030 +8547,2,107000040 +8548,3,107000050 +8549,3,107000060 +8550,3,107000070 +8551,3,107000080 +8552,1,108000010 +8553,2,108000020 +8554,2,108000030 +8555,2,108000040 +8556,3,108000050 +8557,3,108000060 +8558,3,108000070 +8559,3,108000080 +8560,2,180000 +8561,2,170002 +8562,3,170003 +8563,4,170004 +8565,1,101000000 +8566,2,101000001 +8567,3,101000002 +8568,4,101000008 +8569,5,101000004 +8570,1,102000000 +8571,2,102000001 +8572,3,102000002 +8573,4,102000003 +8574,5,102000004 +8575,1,103000000 +8576,2,103000001 +8577,3,103000002 +8578,4,103000003 +8579,5,103000004 +8580,1,105000000 +8581,2,105000001 +8582,3,105000002 +8583,4,105000003 +8584,5,105000004 +8585,1,108000000 +8586,2,108000001 +8587,3,108000002 +8588,4,108000003 +8589,5,108000004 +8590,1,109000000 +8591,2,109000001 +8592,3,109000002 +8593,4,109000003 +8594,5,109000004 +8595,1,111000000 +8596,2,111000001 +8597,3,111000002 +8598,4,111000003 +8599,5,111000004 +8600,1,112000000 +8601,2,112000001 +8602,3,112000002 +8603,4,112000003 +8604,5,112000004 +8605,1,120000000 +8606,2,120000001 +8607,3,120000002 +8608,4,120000003 +8609,5,120000004 +8610,1,101000010 +8611,2,101000020 +8612,2,101000030 +8613,3,101000040 +8614,3,101000050 +8615,3,101000060 +8616,3,101000070 +8617,3,101000080 +8618,1,102000010 +8619,2,102000020 +8620,2,102000030 +8621,2,102000040 +8622,3,102000050 +8623,3,102000060 +8624,3,102000070 +8625,3,102000080 +8626,1,103000010 +8627,2,103000020 +8628,2,103000030 +8629,2,103000040 +8630,3,103000050 +8631,3,103000060 +8632,3,103000070 +8633,3,103000080 +8634,1,104000010 +8635,2,104000020 +8636,2,104000030 +8637,2,104000040 +8638,3,104000050 +8639,3,104000060 +8640,3,104000070 +8641,3,104000080 +8642,1,105000010 +8643,2,105000020 +8644,2,105000030 +8645,2,105000040 +8646,3,105000050 +8647,3,105000060 +8648,3,105000070 +8649,3,105000080 +8650,1,106000010 +8651,2,106000020 +8652,2,106000030 +8653,2,106000040 +8654,3,106000050 +8655,3,106000060 +8656,3,106000070 +8657,3,106000080 +8658,1,107000010 +8659,2,107000020 +8660,2,107000030 +8661,2,107000040 +8662,3,107000050 +8663,3,107000060 +8664,3,107000070 +8665,3,107000080 +8666,1,108000010 +8667,2,108000020 +8668,2,108000030 +8669,2,108000040 +8670,3,108000050 +8671,3,108000060 +8672,3,108000070 +8673,3,108000080 +8674,2,180000 +8675,2,170002 +8676,3,170003 +8677,4,170004 +8679,1,101000000 +8680,2,101000001 +8681,3,101000002 +8682,4,101000008 +8683,5,101000004 +8684,1,102000000 +8685,2,102000001 +8686,3,102000002 +8687,4,102000003 +8688,5,102000004 +8689,1,103000000 +8690,2,103000001 +8691,3,103000002 +8692,4,103000003 +8693,5,103000004 +8694,1,105000000 +8695,2,105000001 +8696,3,105000002 +8697,4,105000003 +8698,5,105000004 +8699,1,108000000 +8700,2,108000001 +8701,3,108000002 +8702,4,108000003 +8703,5,108000004 +8704,1,109000000 +8705,2,109000001 +8706,3,109000002 +8707,4,109000003 +8708,5,109000004 +8709,1,111000000 +8710,2,111000001 +8711,3,111000002 +8712,4,111000003 +8713,5,111000004 +8714,1,112000000 +8715,2,112000001 +8716,3,112000002 +8717,4,112000003 +8718,5,112000004 +8719,1,120000000 +8720,2,120000001 +8721,3,120000002 +8722,4,120000003 +8723,5,120000004 +8724,1,101000010 +8725,2,101000020 +8726,2,101000030 +8727,3,101000040 +8728,3,101000050 +8729,3,101000060 +8730,3,101000070 +8731,3,101000080 +8732,1,102000010 +8733,2,102000020 +8734,2,102000030 +8735,2,102000040 +8736,3,102000050 +8737,3,102000060 +8738,3,102000070 +8739,3,102000080 +8740,1,103000010 +8741,2,103000020 +8742,2,103000030 +8743,2,103000040 +8744,3,103000050 +8745,3,103000060 +8746,3,103000070 +8747,3,103000080 +8748,1,104000010 +8749,2,104000020 +8750,2,104000030 +8751,2,104000040 +8752,3,104000050 +8753,3,104000060 +8754,3,104000070 +8755,3,104000080 +8756,1,105000010 +8757,2,105000020 +8758,2,105000030 +8759,2,105000040 +8760,3,105000050 +8761,3,105000060 +8762,3,105000070 +8763,3,105000080 +8764,1,106000010 +8765,2,106000020 +8766,2,106000030 +8767,2,106000040 +8768,3,106000050 +8769,3,106000060 +8770,3,106000070 +8771,3,106000080 +8772,1,107000010 +8773,2,107000020 +8774,2,107000030 +8775,2,107000040 +8776,3,107000050 +8777,3,107000060 +8778,3,107000070 +8779,3,107000080 +8780,1,108000010 +8781,2,108000020 +8782,2,108000030 +8783,2,108000040 +8784,3,108000050 +8785,3,108000060 +8786,3,108000070 +8787,3,108000080 +8788,2,180001 +8789,2,170002 +8790,3,170003 +8791,4,170004 +8793,1,101000000 +8794,2,101000001 +8795,3,101000002 +8796,4,101000008 +8797,5,101000004 +8798,1,102000000 +8799,2,102000001 +8800,3,102000002 +8801,4,102000003 +8802,5,102000004 +8803,1,103000000 +8804,2,103000001 +8805,3,103000002 +8806,4,103000003 +8807,5,103000004 +8808,1,105000000 +8809,2,105000001 +8810,3,105000002 +8811,4,105000003 +8812,5,105000004 +8813,1,108000000 +8814,2,108000001 +8815,3,108000002 +8816,4,108000003 +8817,5,108000004 +8818,1,109000000 +8819,2,109000001 +8820,3,109000002 +8821,4,109000003 +8822,5,109000004 +8823,1,111000000 +8824,2,111000001 +8825,3,111000002 +8826,4,111000003 +8827,5,111000004 +8828,1,112000000 +8829,2,112000001 +8830,3,112000002 +8831,4,112000003 +8832,5,112000004 +8833,1,120000000 +8834,2,120000001 +8835,3,120000002 +8836,4,120000003 +8837,5,120000004 +8838,1,101000010 +8839,2,101000020 +8840,2,101000030 +8841,3,101000040 +8842,3,101000050 +8843,3,101000060 +8844,3,101000070 +8845,3,101000080 +8846,1,102000010 +8847,2,102000020 +8848,2,102000030 +8849,2,102000040 +8850,3,102000050 +8851,3,102000060 +8852,3,102000070 +8853,3,102000080 +8854,1,103000010 +8855,2,103000020 +8856,2,103000030 +8857,2,103000040 +8858,3,103000050 +8859,3,103000060 +8860,3,103000070 +8861,3,103000080 +8862,1,104000010 +8863,2,104000020 +8864,2,104000030 +8865,2,104000040 +8866,3,104000050 +8867,3,104000060 +8868,3,104000070 +8869,3,104000080 +8870,1,105000010 +8871,2,105000020 +8872,2,105000030 +8873,2,105000040 +8874,3,105000050 +8875,3,105000060 +8876,3,105000070 +8877,3,105000080 +8878,1,106000010 +8879,2,106000020 +8880,2,106000030 +8881,2,106000040 +8882,3,106000050 +8883,3,106000060 +8884,3,106000070 +8885,3,106000080 +8886,1,107000010 +8887,2,107000020 +8888,2,107000030 +8889,2,107000040 +8890,3,107000050 +8891,3,107000060 +8892,3,107000070 +8893,3,107000080 +8894,1,108000010 +8895,2,108000020 +8896,2,108000030 +8897,2,108000040 +8898,3,108000050 +8899,3,108000060 +8900,3,108000070 +8901,3,108000080 +8902,1,109000010 +8903,2,109000020 +8904,2,109000030 +8905,2,109000040 +8906,3,109000050 +8907,3,109000060 +8908,3,109000070 +8909,3,109000080 +8910,2,180001 +8911,2,170002 +8912,3,170003 +8913,4,170004 +8915,1,101000000 +8916,2,101000001 +8917,3,101000002 +8918,4,101000008 +8919,5,101000004 +8920,1,102000000 +8921,2,102000001 +8922,3,102000002 +8923,4,102000003 +8924,5,102000004 +8925,1,103000000 +8926,2,103000001 +8927,3,103000002 +8928,4,103000003 +8929,5,103000004 +8930,1,105000000 +8931,2,105000001 +8932,3,105000002 +8933,4,105000003 +8934,5,105000004 +8935,1,107000000 +8936,2,107000001 +8937,3,107000002 +8938,4,107000003 +8939,5,107000004 +8940,1,108000000 +8941,2,108000001 +8942,3,108000002 +8943,4,108000003 +8944,5,108000004 +8945,1,109000000 +8946,2,109000001 +8947,3,109000002 +8948,4,109000003 +8949,5,109000004 +8950,1,111000000 +8951,2,111000001 +8952,3,111000002 +8953,4,111000003 +8954,5,111000004 +8955,1,112000000 +8956,2,112000001 +8957,3,112000002 +8958,4,112000003 +8959,5,112000004 +8960,1,120000000 +8961,2,120000001 +8962,3,120000002 +8963,4,120000003 +8964,5,120000004 +8965,1,101000010 +8966,2,101000020 +8967,2,101000030 +8968,3,101000040 +8969,3,101000050 +8970,3,101000060 +8971,3,101000070 +8972,3,101000080 +8973,1,102000010 +8974,2,102000020 +8975,2,102000030 +8976,2,102000040 +8977,3,102000050 +8978,3,102000060 +8979,3,102000070 +8980,3,102000080 +8981,1,103000010 +8982,2,103000020 +8983,2,103000030 +8984,2,103000040 +8985,3,103000050 +8986,3,103000060 +8987,3,103000070 +8988,3,103000080 +8989,1,104000010 +8990,2,104000020 +8991,2,104000030 +8992,2,104000040 +8993,3,104000050 +8994,3,104000060 +8995,3,104000070 +8996,3,104000080 +8997,1,105000010 +8998,2,105000020 +8999,2,105000030 +9000,2,105000040 +9001,3,105000050 +9002,3,105000060 +9003,3,105000070 +9004,3,105000080 +9005,1,106000010 +9006,2,106000020 +9007,2,106000030 +9008,2,106000040 +9009,3,106000050 +9010,3,106000060 +9011,3,106000070 +9012,3,106000080 +9013,1,107000010 +9014,2,107000020 +9015,2,107000030 +9016,2,107000040 +9017,3,107000050 +9018,3,107000060 +9019,3,107000070 +9020,3,107000080 +9021,1,108000010 +9022,2,108000020 +9023,2,108000030 +9024,2,108000040 +9025,3,108000050 +9026,3,108000060 +9027,3,108000070 +9028,3,108000080 +9029,1,109000010 +9030,2,109000020 +9031,2,109000030 +9032,2,109000040 +9033,3,109000050 +9034,3,109000060 +9035,3,109000070 +9036,3,109000080 +9037,1,110000010 +9038,2,110000020 +9039,2,110000030 +9040,2,110000040 +9041,3,110000050 +9042,3,110000060 +9043,3,110000070 +9044,3,110000080 +9045,2,180001 +9046,2,170002 +9047,3,170003 +9048,4,170004 +9050,1,101000010 +9051,1,102000010 +9052,1,103000010 +9053,1,104000010 +9054,1,105000010 +9055,1,106000010 +9056,1,107000010 +9057,1,108000010 +9058,1,109000010 +9059,1,110000010 +9060,2,101000020 +9061,2,101000030 +9062,2,102000020 +9063,2,102000030 +9064,2,102000040 +9065,2,103000020 +9066,2,103000030 +9067,2,103000040 +9068,2,104000020 +9069,2,104000030 +9070,2,104000040 +9071,2,105000020 +9072,2,105000030 +9073,2,105000040 +9074,2,106000020 +9075,2,106000030 +9076,2,106000040 +9077,2,107000020 +9078,2,107000030 +9079,2,107000040 +9080,2,108000020 +9081,2,108000030 +9082,2,108000040 +9083,2,109000020 +9084,2,109000030 +9085,2,109000040 +9086,2,110000020 +9087,2,110000030 +9088,2,110000040 +9089,2,118000010 +9090,3,101000050 +9091,3,101000060 +9092,3,101000080 +9093,3,101000040 +9094,3,109000060 +9095,3,109000070 +9096,3,109000080 +9097,3,105000060 +9098,3,105000070 +9099,3,105000080 +9100,3,104000050 +9101,3,106000050 +9102,3,102000060 +9103,3,102000070 +9104,3,102000080 +9105,3,103000050 +9106,3,105000050 +9107,3,107000060 +9108,3,107000070 +9109,3,107000080 +9110,3,108000050 +9111,3,109000050 +9112,3,103000060 +9113,3,103000070 +9114,3,103000080 +9115,3,110000050 +9116,3,106000060 +9117,3,106000070 +9118,3,106000080 +9119,3,101000070 +9120,3,110000060 +9121,3,104000060 +9122,3,104000070 +9123,3,104000080 +9124,3,102000050 +9125,3,104000170 +9126,3,104000260 +9127,3,111000010 +9128,3,111000020 +9129,3,111000030 +9130,3,112000020 +9131,3,112000030 +9132,3,108000060 +9133,3,108000070 +9134,3,108000080 +9135,3,107000050 +9136,3,112000010 +9137,3,110000070 +9138,3,110000080 +9139,3,118000020 +9140,3,118000030 +9141,3,118000040 +9142,4,101000090 +9143,4,101000100 +9144,4,101000110 +9145,4,109000100 +9146,4,105000100 +9147,4,105000110 +9148,4,108000090 +9149,4,110000090 +9150,4,102000100 +9151,4,102000110 +9152,4,106000090 +9153,4,109000090 +9154,4,107000100 +9155,4,103000090 +9156,4,102000090 +9157,4,103000100 +9158,4,106000100 +9159,4,106000110 +9160,4,104000090 +9161,4,104000100 +9162,4,104000110 +9163,4,107000090 +9164,4,104000180 +9165,4,111000040 +9166,4,112000040 +9167,4,108000100 +9168,4,105000090 +9169,4,110000100 +9170,4,118000050 +9171,4,118000060 +9172,5,101000120 +9173,5,109000110 +9174,5,105000120 +9175,5,102000120 +9176,5,107000110 +9177,5,103000120 +9178,5,106000120 +9179,5,104000120 +9180,5,104000190 +9181,5,111000060 +9182,5,112000060 +9183,5,108000110 +9184,5,110000110 +9185,5,118000070 +9186,1,201000010 +9187,1,292000010 +9188,1,299000040 +9189,1,299000070 +9190,1,299000110 +9191,1,299000120 +9192,1,299000140 +9193,2,202000010 +9194,2,290000010 +9195,2,299000010 +9196,2,299000150 +9197,2,299000190 +9198,2,299000200 +9199,2,299000210 +9200,3,298000050 +9201,3,298000060 +9202,3,299000060 +9203,3,299000170 +9204,3,290000120 +9205,3,291000050 +9206,3,292000020 +9207,4,299000670 +9208,4,299000680 +9209,4,204000010 +9210,4,209000040 +9211,5,297000100 +9212,5,291000020 +9213,5,297000130 +9214,5,297000140 +9215,5,203000010 +9216,5,206000030 +9217,1,170002 +9218,1,180002 +9219,2,170003 +9220,2,180003 +9221,3,170004 +9222,3,180004 +9223,4,140000 +9224,5,150010 +9225,5,150020 +9226,5,150030 +9227,5,150040 +9229,1,101000010 +9230,1,102000010 +9231,1,103000010 +9232,1,104000010 +9233,1,105000010 +9234,1,106000010 +9235,1,107000010 +9236,1,108000010 +9237,1,109000010 +9238,1,110000010 +9239,2,101000020 +9240,2,101000030 +9241,2,102000020 +9242,2,102000030 +9243,2,102000040 +9244,2,103000020 +9245,2,103000030 +9246,2,103000040 +9247,2,104000020 +9248,2,104000030 +9249,2,104000040 +9250,2,105000020 +9251,2,105000030 +9252,2,105000040 +9253,2,106000020 +9254,2,106000030 +9255,2,106000040 +9256,2,107000020 +9257,2,107000030 +9258,2,107000040 +9259,2,108000020 +9260,2,108000030 +9261,2,108000040 +9262,2,109000020 +9263,2,109000030 +9264,2,109000040 +9265,2,110000020 +9266,2,110000030 +9267,2,110000040 +9268,2,118000010 +9269,3,101000050 +9270,3,101000060 +9271,3,101000080 +9272,3,101000040 +9273,3,109000060 +9274,3,109000070 +9275,3,109000080 +9276,3,105000060 +9277,3,105000070 +9278,3,105000080 +9279,3,104000050 +9280,3,106000050 +9281,3,102000060 +9282,3,102000070 +9283,3,102000080 +9284,3,103000050 +9285,3,105000050 +9286,3,107000060 +9287,3,107000070 +9288,3,107000080 +9289,3,108000050 +9290,3,109000050 +9291,3,103000060 +9292,3,103000070 +9293,3,103000080 +9294,3,110000050 +9295,3,106000060 +9296,3,106000070 +9297,3,106000080 +9298,3,101000070 +9299,3,110000060 +9300,3,104000060 +9301,3,104000070 +9302,3,104000080 +9303,3,102000050 +9304,3,104000170 +9305,3,104000260 +9306,3,111000010 +9307,3,111000020 +9308,3,111000030 +9309,3,112000020 +9310,3,112000030 +9311,3,108000060 +9312,3,108000070 +9313,3,108000080 +9314,3,107000050 +9315,3,112000010 +9316,3,110000070 +9317,3,110000080 +9318,3,118000020 +9319,3,118000030 +9320,3,118000040 +9321,4,101000090 +9322,4,101000100 +9323,4,101000110 +9324,4,109000100 +9325,4,105000100 +9326,4,105000110 +9327,4,108000090 +9328,4,110000090 +9329,4,102000100 +9330,4,102000110 +9331,4,106000090 +9332,4,109000090 +9333,4,107000100 +9334,4,103000090 +9335,4,102000090 +9336,4,103000100 +9337,4,106000100 +9338,4,106000110 +9339,4,104000090 +9340,4,104000100 +9341,4,104000110 +9342,4,107000090 +9343,4,104000180 +9344,4,111000040 +9345,4,112000040 +9346,4,108000100 +9347,4,105000090 +9348,4,110000100 +9349,4,118000050 +9350,4,118000060 +9351,5,101000120 +9352,5,109000110 +9353,5,105000120 +9354,5,102000120 +9355,5,107000110 +9356,5,103000120 +9357,5,106000120 +9358,5,104000120 +9359,5,104000190 +9360,5,111000060 +9361,5,112000060 +9362,5,108000110 +9363,5,110000110 +9364,5,118000070 +9365,1,201000010 +9366,1,292000010 +9367,1,299000040 +9368,1,299000070 +9369,1,299000110 +9370,1,299000120 +9371,1,299000140 +9372,2,202000010 +9373,2,290000010 +9374,2,299000010 +9375,2,299000150 +9376,2,299000190 +9377,2,299000200 +9378,2,299000210 +9379,3,298000050 +9380,3,298000060 +9381,3,299000060 +9382,3,299000170 +9383,3,290000120 +9384,3,291000050 +9385,3,292000020 +9386,4,299000670 +9387,4,299000680 +9388,4,204000010 +9389,4,209000040 +9390,5,297000100 +9391,5,291000020 +9392,5,297000130 +9393,5,297000140 +9394,5,203000010 +9395,5,206000030 +9396,2,170003 +9397,2,180003 +9398,3,170004 +9399,3,180004 +9400,4,140000 +9401,5,150010 +9402,5,150020 +9403,5,150030 +9404,5,150040 +9406,3,101000050 +9407,3,101000060 +9408,3,101000080 +9409,3,101000040 +9410,3,109000060 +9411,3,109000070 +9412,3,109000080 +9413,3,105000060 +9414,3,105000070 +9415,3,105000080 +9416,3,104000050 +9417,3,106000050 +9418,3,102000060 +9419,3,102000070 +9420,3,102000080 +9421,3,103000050 +9422,3,105000050 +9423,3,107000060 +9424,3,107000070 +9425,3,107000080 +9426,3,108000050 +9427,3,109000050 +9428,3,103000060 +9429,3,103000070 +9430,3,103000080 +9431,3,110000050 +9432,3,106000060 +9433,3,106000070 +9434,3,106000080 +9435,3,101000070 +9436,3,110000060 +9437,3,104000060 +9438,3,104000070 +9439,3,104000080 +9440,3,102000050 +9441,3,104000170 +9442,3,104000260 +9443,3,111000010 +9444,3,111000020 +9445,3,111000030 +9446,3,112000020 +9447,3,112000030 +9448,3,108000060 +9449,3,108000070 +9450,3,108000080 +9451,3,107000050 +9452,3,112000010 +9453,3,110000070 +9454,3,110000080 +9455,3,118000020 +9456,3,118000030 +9457,3,118000040 +9458,4,101000090 +9459,4,101000100 +9460,4,101000110 +9461,4,109000100 +9462,4,105000100 +9463,4,105000110 +9464,4,108000090 +9465,4,110000090 +9466,4,102000100 +9467,4,102000110 +9468,4,106000090 +9469,4,109000090 +9470,4,107000100 +9471,4,103000090 +9472,4,102000090 +9473,4,103000100 +9474,4,106000100 +9475,4,106000110 +9476,4,104000090 +9477,4,104000100 +9478,4,104000110 +9479,4,107000090 +9480,4,104000180 +9481,4,111000040 +9482,4,112000040 +9483,4,108000100 +9484,4,105000090 +9485,4,110000100 +9486,4,118000050 +9487,4,118000060 +9488,5,101000120 +9489,5,109000110 +9490,5,105000120 +9491,5,102000120 +9492,5,107000110 +9493,5,103000120 +9494,5,106000120 +9495,5,104000120 +9496,5,104000190 +9497,5,111000060 +9498,5,112000060 +9499,5,108000110 +9500,5,110000110 +9501,5,118000070 +9502,1,201000010 +9503,1,292000010 +9504,1,299000040 +9505,1,299000070 +9506,1,299000110 +9507,1,299000120 +9508,1,299000140 +9509,2,202000010 +9510,2,290000010 +9511,2,299000010 +9512,2,299000150 +9513,2,299000190 +9514,2,299000200 +9515,2,299000210 +9516,3,298000050 +9517,3,298000060 +9518,3,299000060 +9519,3,299000170 +9520,3,290000120 +9521,3,291000050 +9522,3,292000020 +9523,4,299000670 +9524,4,299000680 +9525,4,204000010 +9526,4,209000040 +9527,5,297000100 +9528,5,291000020 +9529,5,297000130 +9530,5,297000140 +9531,5,203000010 +9532,5,206000030 +9533,3,170004 +9534,3,180004 +9535,4,140000 +9536,5,150010 +9537,5,150020 +9538,5,150030 +9539,5,150040 +9541,3,101000050 +9542,3,101000060 +9543,3,101000080 +9544,3,101000040 +9545,3,109000060 +9546,3,109000070 +9547,3,109000080 +9548,3,105000060 +9549,3,105000070 +9550,3,105000080 +9551,3,104000050 +9552,3,106000050 +9553,3,102000060 +9554,3,102000070 +9555,3,102000080 +9556,3,103000050 +9557,3,105000050 +9558,3,107000060 +9559,3,107000070 +9560,3,107000080 +9561,3,108000050 +9562,3,109000050 +9563,3,103000060 +9564,3,103000070 +9565,3,103000080 +9566,3,110000050 +9567,3,106000060 +9568,3,106000070 +9569,3,106000080 +9570,3,101000070 +9571,3,110000060 +9572,3,104000060 +9573,3,104000070 +9574,3,104000080 +9575,3,102000050 +9576,3,104000170 +9577,3,104000260 +9578,3,111000010 +9579,3,111000020 +9580,3,111000030 +9581,3,112000020 +9582,3,112000030 +9583,3,108000060 +9584,3,108000070 +9585,3,108000080 +9586,3,107000050 +9587,3,112000010 +9588,3,110000070 +9589,3,110000080 +9590,3,118000020 +9591,3,118000030 +9592,3,118000040 +9593,4,101000090 +9594,4,101000100 +9595,4,101000110 +9596,4,109000100 +9597,4,105000100 +9598,4,105000110 +9599,4,108000090 +9600,4,110000090 +9601,4,102000100 +9602,4,102000110 +9603,4,106000090 +9604,4,109000090 +9605,4,107000100 +9606,4,103000090 +9607,4,102000090 +9608,4,103000100 +9609,4,106000100 +9610,4,106000110 +9611,4,104000090 +9612,4,104000100 +9613,4,104000110 +9614,4,107000090 +9615,4,104000180 +9616,4,111000040 +9617,4,112000040 +9618,4,108000100 +9619,4,105000090 +9620,4,110000100 +9621,4,118000050 +9622,4,118000060 +9623,5,101000120 +9624,5,109000110 +9625,5,105000120 +9626,5,102000120 +9627,5,107000110 +9628,5,103000120 +9629,5,106000120 +9630,5,104000120 +9631,5,104000190 +9632,5,111000060 +9633,5,112000060 +9634,5,108000110 +9635,5,110000110 +9636,5,118000070 +9637,1,201000010 +9638,1,292000010 +9639,1,299000040 +9640,1,299000070 +9641,1,299000110 +9642,1,299000120 +9643,1,299000140 +9644,2,202000010 +9645,2,290000010 +9646,2,299000010 +9647,2,299000150 +9648,2,299000190 +9649,2,299000200 +9650,2,299000210 +9651,3,298000050 +9652,3,298000060 +9653,3,299000060 +9654,3,299000170 +9655,3,290000120 +9656,3,291000050 +9657,3,292000020 +9658,4,299000670 +9659,4,299000680 +9660,4,204000010 +9661,4,209000040 +9662,5,297000100 +9663,5,291000020 +9664,5,297000130 +9665,5,297000140 +9666,5,203000010 +9667,5,206000030 +9668,3,170004 +9669,3,180004 +9670,4,140000 +9671,5,150010 +9672,5,150020 +9673,5,150030 +9674,5,150040 +9676,3,101000050 +9677,3,101000060 +9678,3,101000080 +9679,3,101000040 +9680,3,109000060 +9681,3,109000070 +9682,3,109000080 +9683,3,105000060 +9684,3,105000070 +9685,3,105000080 +9686,3,104000050 +9687,3,106000050 +9688,3,102000060 +9689,3,102000070 +9690,3,102000080 +9691,3,103000050 +9692,3,105000050 +9693,3,107000060 +9694,3,107000070 +9695,3,107000080 +9696,3,108000050 +9697,3,109000050 +9698,3,103000060 +9699,3,103000070 +9700,3,103000080 +9701,3,110000050 +9702,3,106000060 +9703,3,106000070 +9704,3,106000080 +9705,3,101000070 +9706,3,110000060 +9707,3,104000060 +9708,3,104000070 +9709,3,104000080 +9710,3,102000050 +9711,3,104000170 +9712,3,104000260 +9713,3,111000010 +9714,3,111000020 +9715,3,111000030 +9716,3,112000020 +9717,3,112000030 +9718,3,108000060 +9719,3,108000070 +9720,3,108000080 +9721,3,107000050 +9722,3,112000010 +9723,3,110000070 +9724,3,110000080 +9725,3,118000020 +9726,3,118000030 +9727,3,118000040 +9728,4,101000090 +9729,4,101000100 +9730,4,101000110 +9731,4,109000100 +9732,4,105000100 +9733,4,105000110 +9734,4,108000090 +9735,4,110000090 +9736,4,102000100 +9737,4,102000110 +9738,4,106000090 +9739,4,109000090 +9740,4,107000100 +9741,4,103000090 +9742,4,102000090 +9743,4,103000100 +9744,4,106000100 +9745,4,106000110 +9746,4,104000090 +9747,4,104000100 +9748,4,104000110 +9749,4,107000090 +9750,4,104000180 +9751,4,111000040 +9752,4,112000040 +9753,4,108000100 +9754,4,105000090 +9755,4,110000100 +9756,4,118000050 +9757,4,118000060 +9758,5,101000120 +9759,5,109000110 +9760,5,105000120 +9761,5,102000120 +9762,5,107000110 +9763,5,103000120 +9764,5,106000120 +9765,5,104000120 +9766,5,104000190 +9767,5,111000060 +9768,5,112000060 +9769,5,108000110 +9770,5,110000110 +9771,5,118000070 +9772,1,201000010 +9773,1,292000010 +9774,1,299000040 +9775,1,299000070 +9776,1,299000110 +9777,1,299000120 +9778,1,299000140 +9779,2,202000010 +9780,2,290000010 +9781,2,299000010 +9782,2,299000150 +9783,2,299000190 +9784,2,299000200 +9785,2,299000210 +9786,3,298000050 +9787,3,298000060 +9788,3,299000060 +9789,3,299000170 +9790,3,290000120 +9791,3,291000050 +9792,3,292000020 +9793,4,299000670 +9794,4,299000680 +9795,4,204000010 +9796,4,209000040 +9797,5,297000100 +9798,5,291000020 +9799,5,297000130 +9800,5,297000140 +9801,5,203000010 +9802,5,206000030 +9803,3,170004 +9804,3,180004 +9805,4,140000 +9806,5,150010 +9807,5,150020 +9808,5,150030 +9809,5,150040 +9810,5,190000 +9811,5,200000 +9812,5,210000 +9814,3,101000050 +9815,3,101000060 +9816,3,101000080 +9817,3,101000040 +9818,3,109000060 +9819,3,109000070 +9820,3,109000080 +9821,3,105000060 +9822,3,105000070 +9823,3,105000080 +9824,3,104000050 +9825,3,106000050 +9826,4,101000090 +9827,4,101000100 +9828,4,101000110 +9829,4,109000100 +9830,4,105000100 +9831,4,105000110 +9832,4,108000090 +9833,4,110000090 +9834,5,101000120 +9835,5,109000110 +9836,5,105000120 +9837,1,101000000 +9838,2,101000001 +9839,3,101000002 +9840,4,101000008 +9841,5,101000004 +9842,1,109000000 +9843,2,109000001 +9844,3,109000002 +9845,4,109000003 +9846,5,109000004 +9847,3,170004 +9848,4,170005 +9849,3,180004 +9850,4,180005 +9851,4,140000 +9852,4,150010 +9853,4,150020 +9854,4,150030 +9855,4,150040 +9857,3,102000060 +9858,3,102000070 +9859,3,102000080 +9860,3,103000050 +9861,3,105000050 +9862,3,107000060 +9863,3,107000070 +9864,3,107000080 +9865,3,108000050 +9866,3,109000050 +9867,3,103000060 +9868,3,103000070 +9869,3,103000080 +9870,3,110000050 +9871,4,102000100 +9872,4,102000350 +9873,4,102000110 +9874,4,106000090 +9875,4,109000090 +9876,4,107000100 +9877,4,103000090 +9878,4,102000090 +9879,4,103000100 +9880,5,102000120 +9881,5,107000110 +9882,5,103000120 +9883,1,102000000 +9884,2,102000001 +9885,3,102000002 +9886,4,102000003 +9887,5,102000004 +9888,1,105000000 +9889,2,105000001 +9890,3,105000002 +9891,4,105000003 +9892,5,105000004 +9893,1,112000000 +9894,2,112000001 +9895,3,112000002 +9896,4,112000003 +9897,5,112000004 +9898,4,110024 +9899,4,110034 +9900,4,110044 +9901,4,110054 +9902,3,110060 +9903,3,110070 +9904,3,110080 +9905,3,110090 +9906,3,110100 +9907,3,110110 +9908,3,110120 +9909,3,110130 +9910,3,110140 +9911,3,110150 +9912,3,110160 +9913,3,110170 +9914,3,110180 +9915,3,110190 +9916,3,110200 +9917,3,110210 +9918,3,110220 +9919,3,110230 +9920,3,110240 +9921,3,110250 +9922,3,110260 +9923,3,110270 +9924,3,110620 +9925,3,110670 +9926,4,140000 +9927,4,150010 +9928,4,150020 +9929,4,150030 +9930,4,150040 +9932,3,118000020 +9933,3,118000030 +9934,3,118000040 +9935,3,106000060 +9936,3,106000070 +9937,3,106000080 +9938,3,101000070 +9939,3,110000060 +9940,3,104000060 +9941,3,104000070 +9942,3,104000080 +9943,3,102000050 +9944,3,104000170 +9945,3,104000260 +9946,4,118000050 +9947,4,118000060 +9948,4,106000100 +9949,4,106000110 +9950,4,104000090 +9951,4,104000100 +9952,4,104000110 +9953,4,104000270 +9954,4,107000090 +9955,4,104000180 +9956,5,118000070 +9957,5,106000120 +9958,5,104000120 +9959,5,104000190 +9960,1,103000000 +9961,2,103000001 +9962,3,103000002 +9963,4,103000003 +9964,5,103000004 +9965,1,111000000 +9966,2,111000001 +9967,3,111000002 +9968,4,111000003 +9969,5,111000004 +9970,1,115000000 +9971,2,115000001 +9972,3,115000002 +9973,4,115000003 +9974,5,115000004 +9975,4,120024 +9976,4,120034 +9977,4,120044 +9978,4,120054 +9979,3,120241 +9980,3,120251 +9981,3,120261 +9982,3,120271 +9983,3,120300 +9984,3,120310 +9985,3,120320 +9986,3,120330 +9987,3,120340 +9988,3,120350 +9989,3,120360 +9990,3,120370 +9991,3,120380 +9992,3,120390 +9993,3,120400 +9994,3,120410 +9995,3,120420 +9996,3,120430 +9997,3,120450 +9998,3,120460 +9999,3,120550 +10000,3,120560 +10001,3,120570 +10002,3,120990 +10003,3,121000 +10004,3,121010 +10005,3,121020 +10006,4,140000 +10007,4,150010 +10008,4,150020 +10009,4,150030 +10010,4,150040 +10012,3,111000010 +10013,3,111000020 +10014,3,111000030 +10015,3,112000020 +10016,3,112000030 +10017,3,108000060 +10018,3,108000070 +10019,3,108000080 +10020,3,107000050 +10021,3,112000010 +10022,3,110000070 +10023,3,110000080 +10024,4,111000040 +10025,4,112000040 +10026,4,108000100 +10027,4,105000090 +10028,4,110000100 +10029,5,111000060 +10030,5,112000060 +10031,5,108000110 +10032,5,110000110 +10033,1,108000000 +10034,2,108000001 +10035,3,108000002 +10036,4,108000003 +10037,5,108000004 +10038,1,107000000 +10039,2,107000001 +10040,3,107000002 +10041,4,107000003 +10042,5,107000004 +10043,1,120000000 +10044,2,120000001 +10045,3,120000002 +10046,4,120000003 +10047,5,120000004 +10048,4,130024 +10049,4,130034 +10050,4,130044 +10051,4,130054 +10052,3,130060 +10053,3,130070 +10054,3,130080 +10055,3,130090 +10056,3,130100 +10057,3,130110 +10058,3,130120 +10059,3,130130 +10060,3,130140 +10061,3,130150 +10062,3,130160 +10063,3,130170 +10064,3,130180 +10065,3,130190 +10066,3,130200 +10067,3,130420 +10068,3,130510 +10069,3,130520 +10070,3,130531 +10071,3,130540 +10072,3,130660 +10073,3,130790 +10074,3,130800 +10075,3,131130 +10076,4,140000 +10077,4,150010 +10078,4,150020 +10079,4,150030 +10080,4,150040 +10082,3,111000010 +10083,3,111000020 +10084,3,111000030 +10085,3,112000020 +10086,3,112000030 +10087,3,108000060 +10088,3,108000070 +10089,3,108000080 +10090,3,107000050 +10091,3,112000010 +10092,3,110000070 +10093,3,110000080 +10094,4,111000040 +10095,4,112000040 +10096,4,108000100 +10097,4,105000090 +10098,4,110000100 +10099,5,111000060 +10100,5,112000060 +10101,5,108000110 +10102,5,110000110 +10103,1,108000000 +10104,2,108000001 +10105,3,108000002 +10106,4,108000003 +10107,5,108000004 +10108,1,107000000 +10109,2,107000001 +10110,3,107000002 +10111,4,107000003 +10112,5,107000004 +10113,1,120000000 +10114,2,120000001 +10115,3,120000002 +10116,4,120000003 +10117,5,120000004 +10118,3,170004 +10119,4,170005 +10120,3,180004 +10121,4,180005 +10122,4,140000 +10123,4,150010 +10124,4,150020 +10125,4,150030 +10126,4,150040 +10128,3,101000050 +10129,3,101000060 +10130,3,101000080 +10131,3,101000040 +10132,3,109000060 +10133,3,109000070 +10134,3,109000080 +10135,3,105000060 +10136,3,105000070 +10137,3,105000080 +10138,3,104000050 +10139,3,106000050 +10140,4,101000090 +10141,4,101000100 +10142,4,101000110 +10143,4,109000100 +10144,4,105000100 +10145,4,105000110 +10146,4,108000090 +10147,4,110000090 +10148,5,101000120 +10149,5,109000110 +10150,5,105000120 +10151,1,101000000 +10152,2,101000001 +10153,3,101000002 +10154,4,101000008 +10155,5,101000004 +10156,1,109000000 +10157,2,109000001 +10158,3,109000002 +10159,4,109000003 +10160,5,109000004 +10161,4,110024 +10162,4,110034 +10163,4,110044 +10164,4,110054 +10165,3,110060 +10166,3,110070 +10167,3,110080 +10168,3,110090 +10169,3,110100 +10170,3,110110 +10171,3,110120 +10172,3,110130 +10173,3,110140 +10174,3,110150 +10175,3,110160 +10176,3,110170 +10177,3,110180 +10178,3,110190 +10179,3,110200 +10180,3,110210 +10181,3,110220 +10182,3,110230 +10183,3,110240 +10184,3,110250 +10185,3,110260 +10186,3,110270 +10187,3,110620 +10188,3,110670 +10189,4,140000 +10190,4,150010 +10191,4,150020 +10192,4,150030 +10193,4,150040 +10195,3,102000060 +10196,3,102000070 +10197,3,102000080 +10198,3,103000050 +10199,3,105000050 +10200,3,107000060 +10201,3,107000070 +10202,3,107000080 +10203,3,108000050 +10204,3,109000050 +10205,3,103000060 +10206,3,103000070 +10207,3,103000080 +10208,3,110000050 +10209,4,102000100 +10210,4,102000110 +10211,4,102000350 +10212,4,106000090 +10213,4,109000090 +10214,4,107000100 +10215,4,103000090 +10216,4,102000090 +10217,4,103000100 +10218,5,102000120 +10219,5,107000110 +10220,5,103000120 +10221,1,102000000 +10222,2,102000001 +10223,3,102000002 +10224,4,102000003 +10225,5,102000004 +10226,1,105000000 +10227,2,105000001 +10228,3,105000002 +10229,4,105000003 +10230,5,105000004 +10231,1,112000000 +10232,2,112000001 +10233,3,112000002 +10234,4,112000003 +10235,5,112000004 +10236,4,120024 +10237,4,120034 +10238,4,120044 +10239,4,120054 +10240,3,120241 +10241,3,120251 +10242,3,120261 +10243,3,120271 +10244,3,120300 +10245,3,120310 +10246,3,120320 +10247,3,120330 +10248,3,120340 +10249,3,120350 +10250,3,120360 +10251,3,120370 +10252,3,120380 +10253,3,120390 +10254,3,120400 +10255,3,120410 +10256,3,120420 +10257,3,120430 +10258,3,120450 +10259,3,120460 +10260,3,120550 +10261,3,120560 +10262,3,120570 +10263,3,120990 +10264,3,121000 +10265,3,121010 +10266,3,121020 +10267,3,121100 +10268,4,140000 +10269,4,150010 +10270,4,150020 +10271,4,150030 +10272,4,150040 +10274,3,118000020 +10275,3,118000030 +10276,3,118000040 +10277,3,106000060 +10278,3,106000070 +10279,3,106000080 +10280,3,101000070 +10281,3,110000060 +10282,3,104000060 +10283,3,104000070 +10284,3,104000080 +10285,3,102000050 +10286,3,104000170 +10287,3,104000260 +10288,4,118000050 +10289,4,118000060 +10290,4,106000100 +10291,4,106000110 +10292,4,104000090 +10293,4,104000100 +10294,4,104000110 +10295,4,104000270 +10296,4,107000090 +10297,4,104000180 +10298,5,118000070 +10299,5,106000120 +10300,5,104000120 +10301,5,104000190 +10302,1,103000000 +10303,2,103000001 +10304,3,103000002 +10305,4,103000003 +10306,5,103000004 +10307,1,111000000 +10308,2,111000001 +10309,3,111000002 +10310,4,111000003 +10311,5,111000004 +10312,1,115000000 +10313,2,115000001 +10314,3,115000002 +10315,4,115000003 +10316,5,115000004 +10317,4,130024 +10318,4,130034 +10319,4,130044 +10320,4,130054 +10321,3,130060 +10322,3,130070 +10323,3,130080 +10324,3,130090 +10325,3,130100 +10326,3,130110 +10327,3,130120 +10328,3,130130 +10329,3,130140 +10330,3,130150 +10331,3,130160 +10332,3,130170 +10333,3,130180 +10334,3,130190 +10335,3,130200 +10336,3,130420 +10337,3,130510 +10338,3,130520 +10339,3,130531 +10340,3,130540 +10341,3,130660 +10342,3,130790 +10343,3,130800 +10344,3,131130 +10345,4,140000 +10346,4,150010 +10347,4,150020 +10348,4,150030 +10349,4,150040 +10351,1,101000010 +10352,1,102000010 +10353,1,103000010 +10354,1,104000010 +10355,1,105000010 +10356,1,106000010 +10357,1,107000010 +10358,1,108000010 +10359,1,109000010 +10360,1,110000010 +10361,2,101000020 +10362,2,101000030 +10363,2,102000020 +10364,2,102000030 +10365,2,102000040 +10366,2,103000020 +10367,2,103000030 +10368,2,103000040 +10369,2,104000020 +10370,2,104000030 +10371,2,104000040 +10372,2,105000020 +10373,2,105000030 +10374,2,105000040 +10375,2,106000020 +10376,2,106000030 +10377,2,106000040 +10378,2,107000020 +10379,2,107000030 +10380,2,107000040 +10381,2,108000020 +10382,2,108000030 +10383,2,108000040 +10384,2,109000020 +10385,2,109000030 +10386,2,109000040 +10387,2,110000020 +10388,2,110000030 +10389,2,110000040 +10390,2,118000010 +10391,3,101000050 +10392,3,101000060 +10393,3,101000080 +10394,3,101000040 +10395,3,109000060 +10396,3,109000070 +10397,3,109000080 +10398,3,105000060 +10399,3,105000070 +10400,3,105000080 +10401,3,104000050 +10402,3,106000050 +10403,3,102000060 +10404,3,102000070 +10405,3,102000080 +10406,3,103000050 +10407,3,105000050 +10408,3,107000060 +10409,3,107000070 +10410,3,107000080 +10411,3,108000050 +10412,3,109000050 +10413,3,103000060 +10414,3,103000070 +10415,3,103000080 +10416,3,110000050 +10417,3,106000060 +10418,3,106000070 +10419,3,106000080 +10420,3,101000070 +10421,3,110000060 +10422,3,104000060 +10423,3,104000070 +10424,3,104000080 +10425,3,102000050 +10426,3,104000170 +10427,3,104000260 +10428,3,111000010 +10429,3,111000020 +10430,3,111000030 +10431,3,112000020 +10432,3,112000030 +10433,3,108000060 +10434,3,108000070 +10435,3,108000080 +10436,3,107000050 +10437,3,112000010 +10438,3,110000070 +10439,3,110000080 +10440,3,118000020 +10441,3,118000030 +10442,3,118000040 +10443,4,101000090 +10444,4,101000100 +10445,4,101000110 +10446,4,109000100 +10447,4,105000100 +10448,4,105000110 +10449,4,108000090 +10450,4,110000090 +10451,4,102000100 +10452,4,102000110 +10453,4,106000090 +10454,4,109000090 +10455,4,107000100 +10456,4,103000090 +10457,4,102000090 +10458,4,103000100 +10459,4,106000100 +10460,4,106000110 +10461,4,104000090 +10462,4,104000100 +10463,4,104000110 +10464,4,107000090 +10465,4,104000180 +10466,4,111000040 +10467,4,112000040 +10468,4,108000100 +10469,4,105000090 +10470,4,110000100 +10471,4,118000050 +10472,4,118000060 +10473,5,101000120 +10474,5,109000110 +10475,5,105000120 +10476,5,102000120 +10477,5,107000110 +10478,5,103000120 +10479,5,106000120 +10480,5,104000120 +10481,5,104000190 +10482,5,111000060 +10483,5,112000060 +10484,5,108000110 +10485,5,110000110 +10486,5,118000070 +10487,1,201000010 +10488,1,292000010 +10489,1,299000040 +10490,1,299000070 +10491,1,299000110 +10492,1,299000120 +10493,1,299000140 +10494,2,202000010 +10495,2,290000010 +10496,2,299000010 +10497,2,299000150 +10498,2,299000190 +10499,2,299000200 +10500,2,299000210 +10501,3,298000050 +10502,3,298000060 +10503,3,299000060 +10504,3,299000170 +10505,3,290000120 +10506,3,291000050 +10507,3,292000020 +10508,4,299000670 +10509,4,299000680 +10510,4,204000010 +10511,4,209000040 +10512,4,202000070 +10513,5,297000100 +10514,5,291000020 +10515,5,297000130 +10516,5,297000140 +10517,5,203000010 +10518,5,206000030 +10519,5,203000050 +10520,1,170002 +10521,1,180002 +10522,2,170003 +10523,2,180003 +10524,3,170004 +10525,3,180004 +10526,4,140000 +10527,5,150010 +10528,5,150020 +10529,5,150030 +10530,5,150040 +10532,1,101000010 +10533,1,102000010 +10534,1,103000010 +10535,1,104000010 +10536,1,105000010 +10537,1,106000010 +10538,1,107000010 +10539,1,108000010 +10540,1,109000010 +10541,1,110000010 +10542,2,101000020 +10543,2,101000030 +10544,2,102000020 +10545,2,102000030 +10546,2,102000040 +10547,2,103000020 +10548,2,103000030 +10549,2,103000040 +10550,2,104000020 +10551,2,104000030 +10552,2,104000040 +10553,2,105000020 +10554,2,105000030 +10555,2,105000040 +10556,2,106000020 +10557,2,106000030 +10558,2,106000040 +10559,2,107000020 +10560,2,107000030 +10561,2,107000040 +10562,2,108000020 +10563,2,108000030 +10564,2,108000040 +10565,2,109000020 +10566,2,109000030 +10567,2,109000040 +10568,2,110000020 +10569,2,110000030 +10570,2,110000040 +10571,2,118000010 +10572,3,101000050 +10573,3,101000060 +10574,3,101000080 +10575,3,101000040 +10576,3,109000060 +10577,3,109000070 +10578,3,109000080 +10579,3,105000060 +10580,3,105000070 +10581,3,105000080 +10582,3,104000050 +10583,3,106000050 +10584,3,102000060 +10585,3,102000070 +10586,3,102000080 +10587,3,103000050 +10588,3,105000050 +10589,3,107000060 +10590,3,107000070 +10591,3,107000080 +10592,3,108000050 +10593,3,109000050 +10594,3,103000060 +10595,3,103000070 +10596,3,103000080 +10597,3,110000050 +10598,3,106000060 +10599,3,106000070 +10600,3,106000080 +10601,3,101000070 +10602,3,110000060 +10603,3,104000060 +10604,3,104000070 +10605,3,104000080 +10606,3,102000050 +10607,3,104000170 +10608,3,104000260 +10609,3,111000010 +10610,3,111000020 +10611,3,111000030 +10612,3,112000020 +10613,3,112000030 +10614,3,108000060 +10615,3,108000070 +10616,3,108000080 +10617,3,107000050 +10618,3,112000010 +10619,3,110000070 +10620,3,110000080 +10621,3,118000020 +10622,3,118000030 +10623,3,118000040 +10624,4,101000090 +10625,4,101000100 +10626,4,101000110 +10627,4,109000100 +10628,4,105000100 +10629,4,105000110 +10630,4,108000090 +10631,4,110000090 +10632,4,102000100 +10633,4,102000110 +10634,4,106000090 +10635,4,109000090 +10636,4,107000100 +10637,4,103000090 +10638,4,102000090 +10639,4,103000100 +10640,4,106000100 +10641,4,106000110 +10642,4,104000090 +10643,4,104000100 +10644,4,104000110 +10645,4,107000090 +10646,4,104000180 +10647,4,111000040 +10648,4,112000040 +10649,4,108000100 +10650,4,105000090 +10651,4,110000100 +10652,4,118000050 +10653,4,118000060 +10654,5,101000120 +10655,5,109000110 +10656,5,105000120 +10657,5,102000120 +10658,5,107000110 +10659,5,103000120 +10660,5,106000120 +10661,5,104000120 +10662,5,104000190 +10663,5,111000060 +10664,5,112000060 +10665,5,108000110 +10666,5,110000110 +10667,5,118000070 +10668,1,201000010 +10669,1,292000010 +10670,1,299000040 +10671,1,299000070 +10672,1,299000110 +10673,1,299000120 +10674,1,299000140 +10675,2,202000010 +10676,2,290000010 +10677,2,299000010 +10678,2,299000150 +10679,2,299000190 +10680,2,299000200 +10681,2,299000210 +10682,3,298000050 +10683,3,298000060 +10684,3,299000060 +10685,3,299000170 +10686,3,290000120 +10687,3,291000050 +10688,3,292000020 +10689,4,299000670 +10690,4,299000680 +10691,4,204000010 +10692,4,209000040 +10693,4,202000070 +10694,5,297000100 +10695,5,291000020 +10696,5,297000130 +10697,5,297000140 +10698,5,203000010 +10699,5,206000030 +10700,5,203000050 +10701,2,170003 +10702,2,180003 +10703,3,170004 +10704,3,180004 +10705,4,140000 +10706,5,150010 +10707,5,150020 +10708,5,150030 +10709,5,150040 +10711,3,101000050 +10712,3,101000060 +10713,3,101000080 +10714,3,101000040 +10715,3,109000060 +10716,3,109000070 +10717,3,109000080 +10718,3,105000060 +10719,3,105000070 +10720,3,105000080 +10721,3,104000050 +10722,3,106000050 +10723,3,102000060 +10724,3,102000070 +10725,3,102000080 +10726,3,103000050 +10727,3,105000050 +10728,3,107000060 +10729,3,107000070 +10730,3,107000080 +10731,3,108000050 +10732,3,109000050 +10733,3,103000060 +10734,3,103000070 +10735,3,103000080 +10736,3,110000050 +10737,3,106000060 +10738,3,106000070 +10739,3,106000080 +10740,3,101000070 +10741,3,110000060 +10742,3,104000060 +10743,3,104000070 +10744,3,104000080 +10745,3,102000050 +10746,3,104000170 +10747,3,104000260 +10748,3,111000010 +10749,3,111000020 +10750,3,111000030 +10751,3,112000020 +10752,3,112000030 +10753,3,108000060 +10754,3,108000070 +10755,3,108000080 +10756,3,107000050 +10757,3,112000010 +10758,3,110000070 +10759,3,110000080 +10760,3,118000020 +10761,3,118000030 +10762,3,118000040 +10763,4,101000090 +10764,4,101000100 +10765,4,101000110 +10766,4,109000100 +10767,4,105000100 +10768,4,105000110 +10769,4,108000090 +10770,4,110000090 +10771,4,102000100 +10772,4,102000110 +10773,4,106000090 +10774,4,109000090 +10775,4,107000100 +10776,4,103000090 +10777,4,102000090 +10778,4,103000100 +10779,4,106000100 +10780,4,106000110 +10781,4,104000090 +10782,4,104000100 +10783,4,104000110 +10784,4,107000090 +10785,4,104000180 +10786,4,111000040 +10787,4,112000040 +10788,4,108000100 +10789,4,105000090 +10790,4,110000100 +10791,4,118000050 +10792,4,118000060 +10793,5,101000120 +10794,5,109000110 +10795,5,105000120 +10796,5,102000120 +10797,5,107000110 +10798,5,103000120 +10799,5,106000120 +10800,5,104000120 +10801,5,104000190 +10802,5,111000060 +10803,5,112000060 +10804,5,108000110 +10805,5,110000110 +10806,5,118000070 +10807,1,201000010 +10808,1,292000010 +10809,1,299000040 +10810,1,299000070 +10811,1,299000110 +10812,1,299000120 +10813,1,299000140 +10814,2,202000010 +10815,2,290000010 +10816,2,299000010 +10817,2,299000150 +10818,2,299000190 +10819,2,299000200 +10820,2,299000210 +10821,3,298000050 +10822,3,298000060 +10823,3,299000060 +10824,3,299000170 +10825,3,290000120 +10826,3,291000050 +10827,3,292000020 +10828,4,299000670 +10829,4,299000680 +10830,4,204000010 +10831,4,209000040 +10832,4,202000070 +10833,5,297000100 +10834,5,291000020 +10835,5,297000130 +10836,5,297000140 +10837,5,203000010 +10838,5,206000030 +10839,5,203000050 +10840,3,170004 +10841,3,180004 +10842,4,140000 +10843,5,150010 +10844,5,150020 +10845,5,150030 +10846,5,150040 +10848,3,101000050 +10849,3,101000060 +10850,3,101000080 +10851,3,101000040 +10852,3,109000060 +10853,3,109000070 +10854,3,109000080 +10855,3,105000060 +10856,3,105000070 +10857,3,105000080 +10858,3,104000050 +10859,3,106000050 +10860,3,102000060 +10861,3,102000070 +10862,3,102000080 +10863,3,103000050 +10864,3,105000050 +10865,3,107000060 +10866,3,107000070 +10867,3,107000080 +10868,3,108000050 +10869,3,109000050 +10870,3,103000060 +10871,3,103000070 +10872,3,103000080 +10873,3,110000050 +10874,3,106000060 +10875,3,106000070 +10876,3,106000080 +10877,3,101000070 +10878,3,110000060 +10879,3,104000060 +10880,3,104000070 +10881,3,104000080 +10882,3,102000050 +10883,3,104000170 +10884,3,104000260 +10885,3,111000010 +10886,3,111000020 +10887,3,111000030 +10888,3,112000020 +10889,3,112000030 +10890,3,108000060 +10891,3,108000070 +10892,3,108000080 +10893,3,107000050 +10894,3,112000010 +10895,3,110000070 +10896,3,110000080 +10897,3,118000020 +10898,3,118000030 +10899,3,118000040 +10900,4,101000090 +10901,4,101000100 +10902,4,101000110 +10903,4,109000100 +10904,4,105000100 +10905,4,105000110 +10906,4,108000090 +10907,4,110000090 +10908,4,102000100 +10909,4,102000110 +10910,4,106000090 +10911,4,109000090 +10912,4,107000100 +10913,4,103000090 +10914,4,102000090 +10915,4,103000100 +10916,4,106000100 +10917,4,106000110 +10918,4,104000090 +10919,4,104000100 +10920,4,104000110 +10921,4,107000090 +10922,4,104000180 +10923,4,111000040 +10924,4,112000040 +10925,4,108000100 +10926,4,105000090 +10927,4,110000100 +10928,4,118000050 +10929,4,118000060 +10930,5,101000120 +10931,5,109000110 +10932,5,105000120 +10933,5,102000120 +10934,5,107000110 +10935,5,103000120 +10936,5,106000120 +10937,5,104000120 +10938,5,104000190 +10939,5,111000060 +10940,5,112000060 +10941,5,108000110 +10942,5,110000110 +10943,5,118000070 +10944,1,201000010 +10945,1,292000010 +10946,1,299000040 +10947,1,299000070 +10948,1,299000110 +10949,1,299000120 +10950,1,299000140 +10951,2,202000010 +10952,2,290000010 +10953,2,299000010 +10954,2,299000150 +10955,2,299000190 +10956,2,299000200 +10957,2,299000210 +10958,3,298000050 +10959,3,298000060 +10960,3,299000060 +10961,3,299000170 +10962,3,290000120 +10963,3,291000050 +10964,3,292000020 +10965,4,299000670 +10966,4,299000680 +10967,4,204000010 +10968,4,209000040 +10969,4,202000070 +10970,5,297000100 +10971,5,291000020 +10972,5,297000130 +10973,5,297000140 +10974,5,203000010 +10975,5,206000030 +10976,5,203000050 +10977,3,170004 +10978,3,180004 +10979,4,140000 +10980,5,150010 +10981,5,150020 +10982,5,150030 +10983,5,150040 +10985,3,101000050 +10986,3,101000060 +10987,3,101000080 +10988,3,101000040 +10989,3,109000060 +10990,3,109000070 +10991,3,109000080 +10992,3,105000060 +10993,3,105000070 +10994,3,105000080 +10995,3,104000050 +10996,3,106000050 +10997,3,102000060 +10998,3,102000070 +10999,3,102000080 +11000,3,103000050 +11001,3,105000050 +11002,3,107000060 +11003,3,107000070 +11004,3,107000080 +11005,3,108000050 +11006,3,109000050 +11007,3,103000060 +11008,3,103000070 +11009,3,103000080 +11010,3,110000050 +11011,3,106000060 +11012,3,106000070 +11013,3,106000080 +11014,3,101000070 +11015,3,110000060 +11016,3,104000060 +11017,3,104000070 +11018,3,104000080 +11019,3,102000050 +11020,3,104000170 +11021,3,104000260 +11022,3,111000010 +11023,3,111000020 +11024,3,111000030 +11025,3,112000020 +11026,3,112000030 +11027,3,108000060 +11028,3,108000070 +11029,3,108000080 +11030,3,107000050 +11031,3,112000010 +11032,3,110000070 +11033,3,110000080 +11034,3,118000020 +11035,3,118000030 +11036,3,118000040 +11037,4,101000090 +11038,4,101000100 +11039,4,101000110 +11040,4,109000100 +11041,4,105000100 +11042,4,105000110 +11043,4,108000090 +11044,4,110000090 +11045,4,102000100 +11046,4,102000110 +11047,4,106000090 +11048,4,109000090 +11049,4,107000100 +11050,4,103000090 +11051,4,102000090 +11052,4,103000100 +11053,4,106000100 +11054,4,106000110 +11055,4,104000090 +11056,4,104000100 +11057,4,104000110 +11058,4,107000090 +11059,4,104000180 +11060,4,111000040 +11061,4,112000040 +11062,4,108000100 +11063,4,105000090 +11064,4,110000100 +11065,4,118000050 +11066,4,118000060 +11067,5,101000120 +11068,5,109000110 +11069,5,105000120 +11070,5,102000120 +11071,5,107000110 +11072,5,103000120 +11073,5,106000120 +11074,5,104000120 +11075,5,104000190 +11076,5,111000060 +11077,5,112000060 +11078,5,108000110 +11079,5,110000110 +11080,5,118000070 +11081,1,201000010 +11082,1,292000010 +11083,1,299000040 +11084,1,299000070 +11085,1,299000110 +11086,1,299000120 +11087,1,299000140 +11088,2,202000010 +11089,2,290000010 +11090,2,299000010 +11091,2,299000150 +11092,2,299000190 +11093,2,299000200 +11094,2,299000210 +11095,3,298000050 +11096,3,298000060 +11097,3,299000060 +11098,3,299000170 +11099,3,290000120 +11100,3,291000050 +11101,3,292000020 +11102,4,299000670 +11103,4,299000680 +11104,4,204000010 +11105,4,209000040 +11106,4,202000070 +11107,5,297000100 +11108,5,291000020 +11109,5,297000130 +11110,5,297000140 +11111,5,203000010 +11112,5,206000030 +11113,5,203000050 +11114,3,170004 +11115,3,180004 +11116,4,140000 +11117,5,150010 +11118,5,150020 +11119,5,150030 +11120,5,150040 +11121,5,190000 +11122,5,200000 +11123,5,210000 +11125,3,118000020 +11126,3,118000030 +11127,3,118000040 +11128,3,106000060 +11129,3,106000070 +11130,3,106000080 +11131,3,101000070 +11132,3,110000060 +11133,3,104000060 +11134,3,104000070 +11135,3,104000080 +11136,3,102000050 +11137,3,104000170 +11138,3,104000260 +11139,4,118000050 +11140,4,118000060 +11141,4,106000100 +11142,4,106000110 +11143,4,104000090 +11144,4,104000100 +11145,4,104000110 +11146,4,104000270 +11147,4,107000090 +11148,4,104000180 +11149,5,118000070 +11150,5,106000120 +11151,5,104000120 +11152,5,104000190 +11153,1,103000000 +11154,2,103000001 +11155,3,103000002 +11156,4,103000003 +11157,5,103000004 +11158,1,111000000 +11159,2,111000001 +11160,3,111000002 +11161,4,111000003 +11162,5,111000004 +11163,1,115000000 +11164,2,115000001 +11165,3,115000002 +11166,4,115000003 +11167,5,115000004 +11168,3,170004 +11169,4,170005 +11170,3,180004 +11171,4,180005 +11172,4,140000 +11173,4,150010 +11174,4,150020 +11175,4,150030 +11176,4,150040 +11178,3,111000010 +11179,3,111000020 +11180,3,111000030 +11181,3,112000020 +11182,3,112000030 +11183,3,108000060 +11184,3,108000070 +11185,3,108000080 +11186,3,107000050 +11187,3,112000010 +11188,3,110000070 +11189,3,110000080 +11190,4,111000040 +11191,4,112000040 +11192,4,108000100 +11193,4,105000090 +11194,4,110000100 +11195,5,111000060 +11196,5,112000060 +11197,5,108000110 +11198,5,110000110 +11199,1,108000000 +11200,2,108000001 +11201,3,108000002 +11202,4,108000003 +11203,5,108000004 +11204,1,107000000 +11205,2,107000001 +11206,3,107000002 +11207,4,107000003 +11208,5,107000004 +11209,1,120000000 +11210,2,120000001 +11211,3,120000002 +11212,4,120000003 +11213,5,120000004 +11214,4,110024 +11215,4,110034 +11216,4,110044 +11217,4,110054 +11218,3,110060 +11219,3,110070 +11220,3,110080 +11221,3,110090 +11222,3,110100 +11223,3,110110 +11224,3,110120 +11225,3,110130 +11226,3,110140 +11227,3,110150 +11228,3,110160 +11229,3,110170 +11230,3,110180 +11231,3,110190 +11232,3,110200 +11233,3,110210 +11234,3,110220 +11235,3,110230 +11236,3,110240 +11237,3,110250 +11238,3,110260 +11239,3,110270 +11240,3,110620 +11241,3,110670 +11242,4,140000 +11243,4,150010 +11244,4,150020 +11245,4,150030 +11246,4,150040 +11248,3,101000050 +11249,3,101000060 +11250,3,101000080 +11251,3,101000040 +11252,3,109000060 +11253,3,109000070 +11254,3,109000080 +11255,3,105000060 +11256,3,105000070 +11257,3,105000080 +11258,3,104000050 +11259,3,106000050 +11260,4,101000090 +11261,4,101000100 +11262,4,101000110 +11263,4,109000100 +11264,4,105000100 +11265,4,105000110 +11266,4,108000090 +11267,4,110000090 +11268,5,101000120 +11269,5,109000110 +11270,5,105000120 +11271,1,101000000 +11272,2,101000001 +11273,3,101000002 +11274,4,101000008 +11275,5,101000004 +11276,1,109000000 +11277,2,109000001 +11278,3,109000002 +11279,4,109000003 +11280,5,109000004 +11281,4,120024 +11282,4,120034 +11283,4,120044 +11284,4,120054 +11285,3,120241 +11286,3,120251 +11287,3,120261 +11288,3,120271 +11289,3,120300 +11290,3,120310 +11291,3,120320 +11292,3,120330 +11293,3,120340 +11294,3,120350 +11295,3,120360 +11296,3,120370 +11297,3,120380 +11298,3,120390 +11299,3,120400 +11300,3,120410 +11301,3,120420 +11302,3,120430 +11303,3,120450 +11304,3,120460 +11305,3,120550 +11306,3,120560 +11307,3,120570 +11308,3,120990 +11309,3,121000 +11310,3,121010 +11311,3,121020 +11312,3,121100 +11313,4,140000 +11314,4,150010 +11315,4,150020 +11316,4,150030 +11317,4,150040 +11319,3,102000060 +11320,3,102000070 +11321,3,102000080 +11322,3,103000050 +11323,3,105000050 +11324,3,107000060 +11325,3,107000070 +11326,3,107000080 +11327,3,108000050 +11328,3,109000050 +11329,3,103000060 +11330,3,103000070 +11331,3,103000080 +11332,3,110000050 +11333,4,102000100 +11334,4,102000110 +11335,4,102000350 +11336,4,106000090 +11337,4,109000090 +11338,4,107000100 +11339,4,103000090 +11340,4,102000090 +11341,4,103000100 +11342,5,102000120 +11343,5,107000110 +11344,5,103000120 +11345,1,102000000 +11346,2,102000001 +11347,3,102000002 +11348,4,102000003 +11349,5,102000004 +11350,1,105000000 +11351,2,105000001 +11352,3,105000002 +11353,4,105000003 +11354,5,105000004 +11355,1,112000000 +11356,2,112000001 +11357,3,112000002 +11358,4,112000003 +11359,5,112000004 +11360,4,130024 +11361,4,130034 +11362,4,130044 +11363,4,130054 +11364,3,130060 +11365,3,130070 +11366,3,130080 +11367,3,130090 +11368,3,130100 +11369,3,130110 +11370,3,130120 +11371,3,130130 +11372,3,130140 +11373,3,130150 +11374,3,130160 +11375,3,130170 +11376,3,130180 +11377,3,130190 +11378,3,130200 +11379,3,130420 +11380,3,130510 +11381,3,130520 +11382,3,130531 +11383,3,130540 +11384,3,130660 +11385,3,130790 +11386,3,130800 +11387,3,131130 +11388,4,140000 +11389,4,150010 +11390,4,150020 +11391,4,150030 +11392,4,150040 +11394,1,101000010 +11395,1,102000010 +11396,1,103000010 +11397,1,104000010 +11398,1,105000010 +11399,1,106000010 +11400,1,107000010 +11401,1,108000010 +11402,1,109000010 +11403,1,110000010 +11404,2,101000020 +11405,2,101000030 +11406,2,102000020 +11407,2,102000030 +11408,2,102000040 +11409,2,103000020 +11410,2,103000030 +11411,2,103000040 +11412,2,104000020 +11413,2,104000030 +11414,2,104000040 +11415,2,105000020 +11416,2,105000030 +11417,2,105000040 +11418,2,106000020 +11419,2,106000030 +11420,2,106000040 +11421,2,107000020 +11422,2,107000030 +11423,2,107000040 +11424,2,108000020 +11425,2,108000030 +11426,2,108000040 +11427,2,109000020 +11428,2,109000030 +11429,2,109000040 +11430,2,110000020 +11431,2,110000030 +11432,2,110000040 +11433,2,118000010 +11434,3,101000050 +11435,3,101000060 +11436,3,101000080 +11437,3,101000040 +11438,3,109000060 +11439,3,109000070 +11440,3,109000080 +11441,3,105000060 +11442,3,105000070 +11443,3,105000080 +11444,3,104000050 +11445,3,106000050 +11446,3,102000060 +11447,3,102000070 +11448,3,102000080 +11449,3,103000050 +11450,3,105000050 +11451,3,107000060 +11452,3,107000070 +11453,3,107000080 +11454,3,108000050 +11455,3,109000050 +11456,3,103000060 +11457,3,103000070 +11458,3,103000080 +11459,3,110000050 +11460,3,106000060 +11461,3,106000070 +11462,3,106000080 +11463,3,101000070 +11464,3,110000060 +11465,3,104000060 +11466,3,104000070 +11467,3,104000080 +11468,3,102000050 +11469,3,104000170 +11470,3,104000260 +11471,3,111000010 +11472,3,111000020 +11473,3,111000030 +11474,3,112000020 +11475,3,112000030 +11476,3,108000060 +11477,3,108000070 +11478,3,108000080 +11479,3,107000050 +11480,3,112000010 +11481,3,110000070 +11482,3,110000080 +11483,3,118000020 +11484,3,118000030 +11485,3,118000040 +11486,4,101000090 +11487,4,101000100 +11488,4,101000110 +11489,4,109000100 +11490,4,105000100 +11491,4,105000110 +11492,4,108000090 +11493,4,110000090 +11494,4,102000100 +11495,4,102000110 +11496,4,106000090 +11497,4,109000090 +11498,4,107000100 +11499,4,103000090 +11500,4,102000090 +11501,4,103000100 +11502,4,106000100 +11503,4,106000110 +11504,4,104000090 +11505,4,104000100 +11506,4,104000110 +11507,4,107000090 +11508,4,104000180 +11509,4,111000040 +11510,4,112000040 +11511,4,108000100 +11512,4,105000090 +11513,4,110000100 +11514,4,118000050 +11515,4,118000060 +11516,5,101000120 +11517,5,109000110 +11518,5,105000120 +11519,5,102000120 +11520,5,107000110 +11521,5,103000120 +11522,5,106000120 +11523,5,104000120 +11524,5,104000190 +11525,5,111000060 +11526,5,112000060 +11527,5,108000110 +11528,5,110000110 +11529,5,118000070 +11530,1,201000010 +11531,1,292000010 +11532,1,299000040 +11533,1,299000070 +11534,1,299000110 +11535,1,299000120 +11536,1,299000140 +11537,2,202000010 +11538,2,290000010 +11539,2,299000010 +11540,2,299000150 +11541,2,299000190 +11542,2,299000200 +11543,2,299000210 +11544,3,298000050 +11545,3,298000060 +11546,3,299000060 +11547,3,299000170 +11548,3,290000120 +11549,3,291000050 +11550,3,292000020 +11551,4,299000670 +11552,4,299000680 +11553,4,204000010 +11554,4,209000040 +11555,4,202000070 +11556,4,209000070 +11557,5,297000100 +11558,5,291000020 +11559,5,297000130 +11560,5,297000140 +11561,5,203000010 +11562,5,206000030 +11563,5,203000050 +11564,5,202000090 +11565,1,170002 +11566,1,180002 +11567,2,170003 +11568,2,180003 +11569,3,170004 +11570,3,180004 +11571,4,140000 +11572,5,150010 +11573,5,150020 +11574,5,150030 +11575,5,150040 +11577,1,101000010 +11578,1,102000010 +11579,1,103000010 +11580,1,104000010 +11581,1,105000010 +11582,1,106000010 +11583,1,107000010 +11584,1,108000010 +11585,1,109000010 +11586,1,110000010 +11587,2,101000020 +11588,2,101000030 +11589,2,102000020 +11590,2,102000030 +11591,2,102000040 +11592,2,103000020 +11593,2,103000030 +11594,2,103000040 +11595,2,104000020 +11596,2,104000030 +11597,2,104000040 +11598,2,105000020 +11599,2,105000030 +11600,2,105000040 +11601,2,106000020 +11602,2,106000030 +11603,2,106000040 +11604,2,107000020 +11605,2,107000030 +11606,2,107000040 +11607,2,108000020 +11608,2,108000030 +11609,2,108000040 +11610,2,109000020 +11611,2,109000030 +11612,2,109000040 +11613,2,110000020 +11614,2,110000030 +11615,2,110000040 +11616,2,118000010 +11617,3,101000050 +11618,3,101000060 +11619,3,101000080 +11620,3,101000040 +11621,3,109000060 +11622,3,109000070 +11623,3,109000080 +11624,3,105000060 +11625,3,105000070 +11626,3,105000080 +11627,3,104000050 +11628,3,106000050 +11629,3,102000060 +11630,3,102000070 +11631,3,102000080 +11632,3,103000050 +11633,3,105000050 +11634,3,107000060 +11635,3,107000070 +11636,3,107000080 +11637,3,108000050 +11638,3,109000050 +11639,3,103000060 +11640,3,103000070 +11641,3,103000080 +11642,3,110000050 +11643,3,106000060 +11644,3,106000070 +11645,3,106000080 +11646,3,101000070 +11647,3,110000060 +11648,3,104000060 +11649,3,104000070 +11650,3,104000080 +11651,3,102000050 +11652,3,104000170 +11653,3,104000260 +11654,3,111000010 +11655,3,111000020 +11656,3,111000030 +11657,3,112000020 +11658,3,112000030 +11659,3,108000060 +11660,3,108000070 +11661,3,108000080 +11662,3,107000050 +11663,3,112000010 +11664,3,110000070 +11665,3,110000080 +11666,3,118000020 +11667,3,118000030 +11668,3,118000040 +11669,4,101000090 +11670,4,101000100 +11671,4,101000110 +11672,4,109000100 +11673,4,105000100 +11674,4,105000110 +11675,4,108000090 +11676,4,110000090 +11677,4,102000100 +11678,4,102000110 +11679,4,106000090 +11680,4,109000090 +11681,4,107000100 +11682,4,103000090 +11683,4,102000090 +11684,4,103000100 +11685,4,106000100 +11686,4,106000110 +11687,4,104000090 +11688,4,104000100 +11689,4,104000110 +11690,4,107000090 +11691,4,104000180 +11692,4,111000040 +11693,4,112000040 +11694,4,108000100 +11695,4,105000090 +11696,4,110000100 +11697,4,118000050 +11698,4,118000060 +11699,5,101000120 +11700,5,109000110 +11701,5,105000120 +11702,5,102000120 +11703,5,107000110 +11704,5,103000120 +11705,5,106000120 +11706,5,104000120 +11707,5,104000190 +11708,5,111000060 +11709,5,112000060 +11710,5,108000110 +11711,5,110000110 +11712,5,118000070 +11713,1,201000010 +11714,1,292000010 +11715,1,299000040 +11716,1,299000070 +11717,1,299000110 +11718,1,299000120 +11719,1,299000140 +11720,2,202000010 +11721,2,290000010 +11722,2,299000010 +11723,2,299000150 +11724,2,299000190 +11725,2,299000200 +11726,2,299000210 +11727,3,298000050 +11728,3,298000060 +11729,3,299000060 +11730,3,299000170 +11731,3,290000120 +11732,3,291000050 +11733,3,292000020 +11734,4,299000670 +11735,4,299000680 +11736,4,204000010 +11737,4,209000040 +11738,4,202000070 +11739,4,209000070 +11740,5,297000100 +11741,5,291000020 +11742,5,297000130 +11743,5,297000140 +11744,5,203000010 +11745,5,206000030 +11746,5,203000050 +11747,5,202000090 +11748,2,170003 +11749,2,180003 +11750,3,170004 +11751,3,180004 +11752,4,140000 +11753,5,150010 +11754,5,150020 +11755,5,150030 +11756,5,150040 +11758,3,101000050 +11759,3,101000060 +11760,3,101000080 +11761,3,101000040 +11762,3,109000060 +11763,3,109000070 +11764,3,109000080 +11765,3,105000060 +11766,3,105000070 +11767,3,105000080 +11768,3,104000050 +11769,3,106000050 +11770,3,102000060 +11771,3,102000070 +11772,3,102000080 +11773,3,103000050 +11774,3,105000050 +11775,3,107000060 +11776,3,107000070 +11777,3,107000080 +11778,3,108000050 +11779,3,109000050 +11780,3,103000060 +11781,3,103000070 +11782,3,103000080 +11783,3,110000050 +11784,3,106000060 +11785,3,106000070 +11786,3,106000080 +11787,3,101000070 +11788,3,110000060 +11789,3,104000060 +11790,3,104000070 +11791,3,104000080 +11792,3,102000050 +11793,3,104000170 +11794,3,104000260 +11795,3,111000010 +11796,3,111000020 +11797,3,111000030 +11798,3,112000020 +11799,3,112000030 +11800,3,108000060 +11801,3,108000070 +11802,3,108000080 +11803,3,107000050 +11804,3,112000010 +11805,3,110000070 +11806,3,110000080 +11807,3,118000020 +11808,3,118000030 +11809,3,118000040 +11810,4,101000090 +11811,4,101000100 +11812,4,101000110 +11813,4,109000100 +11814,4,105000100 +11815,4,105000110 +11816,4,108000090 +11817,4,110000090 +11818,4,102000100 +11819,4,102000110 +11820,4,106000090 +11821,4,109000090 +11822,4,107000100 +11823,4,103000090 +11824,4,102000090 +11825,4,103000100 +11826,4,106000100 +11827,4,106000110 +11828,4,104000090 +11829,4,104000100 +11830,4,104000110 +11831,4,107000090 +11832,4,104000180 +11833,4,111000040 +11834,4,112000040 +11835,4,108000100 +11836,4,105000090 +11837,4,110000100 +11838,4,118000050 +11839,4,118000060 +11840,5,101000120 +11841,5,109000110 +11842,5,105000120 +11843,5,102000120 +11844,5,107000110 +11845,5,103000120 +11846,5,106000120 +11847,5,104000120 +11848,5,104000190 +11849,5,111000060 +11850,5,112000060 +11851,5,108000110 +11852,5,110000110 +11853,5,118000070 +11854,1,201000010 +11855,1,292000010 +11856,1,299000040 +11857,1,299000070 +11858,1,299000110 +11859,1,299000120 +11860,1,299000140 +11861,2,202000010 +11862,2,290000010 +11863,2,299000010 +11864,2,299000150 +11865,2,299000190 +11866,2,299000200 +11867,2,299000210 +11868,3,298000050 +11869,3,298000060 +11870,3,299000060 +11871,3,299000170 +11872,3,290000120 +11873,3,291000050 +11874,3,292000020 +11875,4,299000670 +11876,4,299000680 +11877,4,204000010 +11878,4,209000040 +11879,4,202000070 +11880,4,209000070 +11881,5,297000100 +11882,5,291000020 +11883,5,297000130 +11884,5,297000140 +11885,5,203000010 +11886,5,206000030 +11887,5,203000050 +11888,5,202000090 +11889,3,170004 +11890,3,180004 +11891,4,140000 +11892,5,150010 +11893,5,150020 +11894,5,150030 +11895,5,150040 +11897,3,101000050 +11898,3,101000060 +11899,3,101000080 +11900,3,101000040 +11901,3,109000060 +11902,3,109000070 +11903,3,109000080 +11904,3,105000060 +11905,3,105000070 +11906,3,105000080 +11907,3,104000050 +11908,3,106000050 +11909,3,102000060 +11910,3,102000070 +11911,3,102000080 +11912,3,103000050 +11913,3,105000050 +11914,3,107000060 +11915,3,107000070 +11916,3,107000080 +11917,3,108000050 +11918,3,109000050 +11919,3,103000060 +11920,3,103000070 +11921,3,103000080 +11922,3,110000050 +11923,3,106000060 +11924,3,106000070 +11925,3,106000080 +11926,3,101000070 +11927,3,110000060 +11928,3,104000060 +11929,3,104000070 +11930,3,104000080 +11931,3,102000050 +11932,3,104000170 +11933,3,104000260 +11934,3,111000010 +11935,3,111000020 +11936,3,111000030 +11937,3,112000020 +11938,3,112000030 +11939,3,108000060 +11940,3,108000070 +11941,3,108000080 +11942,3,107000050 +11943,3,112000010 +11944,3,110000070 +11945,3,110000080 +11946,3,118000020 +11947,3,118000030 +11948,3,118000040 +11949,4,101000090 +11950,4,101000100 +11951,4,101000110 +11952,4,109000100 +11953,4,105000100 +11954,4,105000110 +11955,4,108000090 +11956,4,110000090 +11957,4,102000100 +11958,4,102000110 +11959,4,106000090 +11960,4,109000090 +11961,4,107000100 +11962,4,103000090 +11963,4,102000090 +11964,4,103000100 +11965,4,106000100 +11966,4,106000110 +11967,4,104000090 +11968,4,104000100 +11969,4,104000110 +11970,4,107000090 +11971,4,104000180 +11972,4,111000040 +11973,4,112000040 +11974,4,108000100 +11975,4,105000090 +11976,4,110000100 +11977,4,118000050 +11978,4,118000060 +11979,5,101000120 +11980,5,109000110 +11981,5,105000120 +11982,5,102000120 +11983,5,107000110 +11984,5,103000120 +11985,5,106000120 +11986,5,104000120 +11987,5,104000190 +11988,5,111000060 +11989,5,112000060 +11990,5,108000110 +11991,5,110000110 +11992,5,118000070 +11993,1,201000010 +11994,1,292000010 +11995,1,299000040 +11996,1,299000070 +11997,1,299000110 +11998,1,299000120 +11999,1,299000140 +12000,2,202000010 +12001,2,290000010 +12002,2,299000010 +12003,2,299000150 +12004,2,299000190 +12005,2,299000200 +12006,2,299000210 +12007,3,298000050 +12008,3,298000060 +12009,3,299000060 +12010,3,299000170 +12011,3,290000120 +12012,3,291000050 +12013,3,292000020 +12014,4,299000670 +12015,4,299000680 +12016,4,204000010 +12017,4,209000040 +12018,4,202000070 +12019,4,209000070 +12020,5,297000100 +12021,5,291000020 +12022,5,297000130 +12023,5,297000140 +12024,5,203000010 +12025,5,206000030 +12026,5,203000050 +12027,5,202000090 +12028,3,170004 +12029,3,180004 +12030,4,140000 +12031,5,150010 +12032,5,150020 +12033,5,150030 +12034,5,150040 +12036,3,101000050 +12037,3,101000060 +12038,3,101000080 +12039,3,101000040 +12040,3,109000060 +12041,3,109000070 +12042,3,109000080 +12043,3,105000060 +12044,3,105000070 +12045,3,105000080 +12046,3,104000050 +12047,3,106000050 +12048,3,102000060 +12049,3,102000070 +12050,3,102000080 +12051,3,103000050 +12052,3,105000050 +12053,3,107000060 +12054,3,107000070 +12055,3,107000080 +12056,3,108000050 +12057,3,109000050 +12058,3,103000060 +12059,3,103000070 +12060,3,103000080 +12061,3,110000050 +12062,3,106000060 +12063,3,106000070 +12064,3,106000080 +12065,3,101000070 +12066,3,110000060 +12067,3,104000060 +12068,3,104000070 +12069,3,104000080 +12070,3,102000050 +12071,3,104000170 +12072,3,104000260 +12073,3,111000010 +12074,3,111000020 +12075,3,111000030 +12076,3,112000020 +12077,3,112000030 +12078,3,108000060 +12079,3,108000070 +12080,3,108000080 +12081,3,107000050 +12082,3,112000010 +12083,3,110000070 +12084,3,110000080 +12085,3,118000020 +12086,3,118000030 +12087,3,118000040 +12088,4,101000090 +12089,4,101000100 +12090,4,101000110 +12091,4,109000100 +12092,4,105000100 +12093,4,105000110 +12094,4,108000090 +12095,4,110000090 +12096,4,102000100 +12097,4,102000110 +12098,4,106000090 +12099,4,109000090 +12100,4,107000100 +12101,4,103000090 +12102,4,102000090 +12103,4,103000100 +12104,4,106000100 +12105,4,106000110 +12106,4,104000090 +12107,4,104000100 +12108,4,104000110 +12109,4,107000090 +12110,4,104000180 +12111,4,111000040 +12112,4,112000040 +12113,4,108000100 +12114,4,105000090 +12115,4,110000100 +12116,4,118000050 +12117,4,118000060 +12118,5,101000120 +12119,5,109000110 +12120,5,105000120 +12121,5,102000120 +12122,5,107000110 +12123,5,103000120 +12124,5,106000120 +12125,5,104000120 +12126,5,104000190 +12127,5,111000060 +12128,5,112000060 +12129,5,108000110 +12130,5,110000110 +12131,5,118000070 +12132,1,201000010 +12133,1,292000010 +12134,1,299000040 +12135,1,299000070 +12136,1,299000110 +12137,1,299000120 +12138,1,299000140 +12139,2,202000010 +12140,2,290000010 +12141,2,299000010 +12142,2,299000150 +12143,2,299000190 +12144,2,299000200 +12145,2,299000210 +12146,3,298000050 +12147,3,298000060 +12148,3,299000060 +12149,3,299000170 +12150,3,290000120 +12151,3,291000050 +12152,3,292000020 +12153,4,299000670 +12154,4,299000680 +12155,4,204000010 +12156,4,209000040 +12157,4,202000070 +12158,4,209000070 +12159,5,297000100 +12160,5,291000020 +12161,5,297000130 +12162,5,297000140 +12163,5,203000010 +12164,5,206000030 +12165,5,203000050 +12166,5,202000090 +12167,3,170004 +12168,3,180004 +12169,4,140000 +12170,5,150010 +12171,5,150020 +12172,5,150030 +12173,5,150040 +12174,5,190000 +12175,5,200000 +12176,5,210000 +12178,1,101000010 +12179,1,102000010 +12180,1,103000010 +12181,1,104000010 +12182,1,105000010 +12183,1,106000010 +12184,1,107000010 +12185,1,108000010 +12186,1,109000010 +12187,1,110000010 +12188,2,101000020 +12189,2,101000030 +12190,2,102000020 +12191,2,102000030 +12192,2,102000040 +12193,2,103000020 +12194,2,103000030 +12195,2,103000040 +12196,2,104000020 +12197,2,104000030 +12198,2,104000040 +12199,2,105000020 +12200,2,105000030 +12201,2,105000040 +12202,2,106000020 +12203,2,106000030 +12204,2,106000040 +12205,2,107000020 +12206,2,107000030 +12207,2,107000040 +12208,2,108000020 +12209,2,108000030 +12210,2,108000040 +12211,2,109000020 +12212,2,109000030 +12213,2,109000040 +12214,2,110000020 +12215,2,110000030 +12216,2,110000040 +12217,2,118000010 +12218,3,101000050 +12219,3,101000060 +12220,3,101000080 +12221,3,101000040 +12222,3,109000060 +12223,3,109000070 +12224,3,109000080 +12225,3,105000060 +12226,3,105000070 +12227,3,105000080 +12228,3,104000050 +12229,3,106000050 +12230,3,102000060 +12231,3,102000070 +12232,3,102000080 +12233,3,103000050 +12234,3,105000050 +12235,3,107000060 +12236,3,107000070 +12237,3,107000080 +12238,3,108000050 +12239,3,109000050 +12240,3,103000060 +12241,3,103000070 +12242,3,103000080 +12243,3,110000050 +12244,3,106000060 +12245,3,106000070 +12246,3,106000080 +12247,3,101000070 +12248,3,110000060 +12249,3,104000060 +12250,3,104000070 +12251,3,104000080 +12252,3,102000050 +12253,3,104000170 +12254,3,104000260 +12255,3,111000010 +12256,3,111000020 +12257,3,111000030 +12258,3,112000020 +12259,3,112000030 +12260,3,108000060 +12261,3,108000070 +12262,3,108000080 +12263,3,107000050 +12264,3,112000010 +12265,3,110000070 +12266,3,110000080 +12267,3,118000020 +12268,3,118000030 +12269,3,118000040 +12270,4,101000090 +12271,4,101000100 +12272,4,101000110 +12273,4,109000100 +12274,4,105000100 +12275,4,105000110 +12276,4,108000090 +12277,4,110000090 +12278,4,102000100 +12279,4,102000110 +12280,4,106000090 +12281,4,109000090 +12282,4,107000100 +12283,4,103000090 +12284,4,102000090 +12285,4,103000100 +12286,4,106000100 +12287,4,106000110 +12288,4,104000090 +12289,4,104000100 +12290,4,104000110 +12291,4,107000090 +12292,4,104000180 +12293,4,111000040 +12294,4,112000040 +12295,4,108000100 +12296,4,105000090 +12297,4,110000100 +12298,4,118000050 +12299,4,118000060 +12300,5,101000120 +12301,5,109000110 +12302,5,105000120 +12303,5,102000120 +12304,5,107000110 +12305,5,103000120 +12306,5,106000120 +12307,5,104000120 +12308,5,104000190 +12309,5,111000060 +12310,5,112000060 +12311,5,108000110 +12312,5,110000110 +12313,5,118000070 +12314,1,201000010 +12315,1,292000010 +12316,1,299000040 +12317,1,299000070 +12318,1,299000110 +12319,1,299000120 +12320,1,299000140 +12321,2,202000010 +12322,2,290000010 +12323,2,299000010 +12324,2,299000150 +12325,2,299000190 +12326,2,299000200 +12327,2,299000210 +12328,3,298000050 +12329,3,298000060 +12330,3,299000060 +12331,3,299000170 +12332,3,290000120 +12333,3,291000050 +12334,3,292000020 +12335,4,299000670 +12336,4,299000680 +12337,4,204000010 +12338,4,209000040 +12339,4,202000070 +12340,4,209000070 +12341,4,203000110 +12342,5,297000100 +12343,5,291000020 +12344,5,297000130 +12345,5,297000140 +12346,5,203000010 +12347,5,206000030 +12348,5,203000050 +12349,5,202000090 +12350,5,204000080 +12351,1,170002 +12352,1,180002 +12353,2,170003 +12354,2,180003 +12355,3,170004 +12356,3,180004 +12357,4,140000 +12358,5,150010 +12359,5,150020 +12360,5,150030 +12361,5,150040 +12363,1,101000010 +12364,1,102000010 +12365,1,103000010 +12366,1,104000010 +12367,1,105000010 +12368,1,106000010 +12369,1,107000010 +12370,1,108000010 +12371,1,109000010 +12372,1,110000010 +12373,2,101000020 +12374,2,101000030 +12375,2,102000020 +12376,2,102000030 +12377,2,102000040 +12378,2,103000020 +12379,2,103000030 +12380,2,103000040 +12381,2,104000020 +12382,2,104000030 +12383,2,104000040 +12384,2,105000020 +12385,2,105000030 +12386,2,105000040 +12387,2,106000020 +12388,2,106000030 +12389,2,106000040 +12390,2,107000020 +12391,2,107000030 +12392,2,107000040 +12393,2,108000020 +12394,2,108000030 +12395,2,108000040 +12396,2,109000020 +12397,2,109000030 +12398,2,109000040 +12399,2,110000020 +12400,2,110000030 +12401,2,110000040 +12402,2,118000010 +12403,3,101000050 +12404,3,101000060 +12405,3,101000080 +12406,3,101000040 +12407,3,109000060 +12408,3,109000070 +12409,3,109000080 +12410,3,105000060 +12411,3,105000070 +12412,3,105000080 +12413,3,104000050 +12414,3,106000050 +12415,3,102000060 +12416,3,102000070 +12417,3,102000080 +12418,3,103000050 +12419,3,105000050 +12420,3,107000060 +12421,3,107000070 +12422,3,107000080 +12423,3,108000050 +12424,3,109000050 +12425,3,103000060 +12426,3,103000070 +12427,3,103000080 +12428,3,110000050 +12429,3,106000060 +12430,3,106000070 +12431,3,106000080 +12432,3,101000070 +12433,3,110000060 +12434,3,104000060 +12435,3,104000070 +12436,3,104000080 +12437,3,102000050 +12438,3,104000170 +12439,3,104000260 +12440,3,111000010 +12441,3,111000020 +12442,3,111000030 +12443,3,112000020 +12444,3,112000030 +12445,3,108000060 +12446,3,108000070 +12447,3,108000080 +12448,3,107000050 +12449,3,112000010 +12450,3,110000070 +12451,3,110000080 +12452,3,118000020 +12453,3,118000030 +12454,3,118000040 +12455,4,101000090 +12456,4,101000100 +12457,4,101000110 +12458,4,109000100 +12459,4,105000100 +12460,4,105000110 +12461,4,108000090 +12462,4,110000090 +12463,4,102000100 +12464,4,102000110 +12465,4,106000090 +12466,4,109000090 +12467,4,107000100 +12468,4,103000090 +12469,4,102000090 +12470,4,103000100 +12471,4,106000100 +12472,4,106000110 +12473,4,104000090 +12474,4,104000100 +12475,4,104000110 +12476,4,107000090 +12477,4,104000180 +12478,4,111000040 +12479,4,112000040 +12480,4,108000100 +12481,4,105000090 +12482,4,110000100 +12483,4,118000050 +12484,4,118000060 +12485,5,101000120 +12486,5,109000110 +12487,5,105000120 +12488,5,102000120 +12489,5,107000110 +12490,5,103000120 +12491,5,106000120 +12492,5,104000120 +12493,5,104000190 +12494,5,111000060 +12495,5,112000060 +12496,5,108000110 +12497,5,110000110 +12498,5,118000070 +12499,1,201000010 +12500,1,292000010 +12501,1,299000040 +12502,1,299000070 +12503,1,299000110 +12504,1,299000120 +12505,1,299000140 +12506,2,202000010 +12507,2,290000010 +12508,2,299000010 +12509,2,299000150 +12510,2,299000190 +12511,2,299000200 +12512,2,299000210 +12513,3,298000050 +12514,3,298000060 +12515,3,299000060 +12516,3,299000170 +12517,3,290000120 +12518,3,291000050 +12519,3,292000020 +12520,4,299000670 +12521,4,299000680 +12522,4,204000010 +12523,4,209000040 +12524,4,202000070 +12525,4,209000070 +12526,4,203000110 +12527,5,297000100 +12528,5,291000020 +12529,5,297000130 +12530,5,297000140 +12531,5,203000010 +12532,5,206000030 +12533,5,203000050 +12534,5,202000090 +12535,5,204000080 +12536,2,170003 +12537,2,180003 +12538,3,170004 +12539,3,180004 +12540,4,140000 +12541,5,150010 +12542,5,150020 +12543,5,150030 +12544,5,150040 +12546,3,101000050 +12547,3,101000060 +12548,3,101000080 +12549,3,101000040 +12550,3,109000060 +12551,3,109000070 +12552,3,109000080 +12553,3,105000060 +12554,3,105000070 +12555,3,105000080 +12556,3,104000050 +12557,3,106000050 +12558,3,102000060 +12559,3,102000070 +12560,3,102000080 +12561,3,103000050 +12562,3,105000050 +12563,3,107000060 +12564,3,107000070 +12565,3,107000080 +12566,3,108000050 +12567,3,109000050 +12568,3,103000060 +12569,3,103000070 +12570,3,103000080 +12571,3,110000050 +12572,3,106000060 +12573,3,106000070 +12574,3,106000080 +12575,3,101000070 +12576,3,110000060 +12577,3,104000060 +12578,3,104000070 +12579,3,104000080 +12580,3,102000050 +12581,3,104000170 +12582,3,104000260 +12583,3,111000010 +12584,3,111000020 +12585,3,111000030 +12586,3,112000020 +12587,3,112000030 +12588,3,108000060 +12589,3,108000070 +12590,3,108000080 +12591,3,107000050 +12592,3,112000010 +12593,3,110000070 +12594,3,110000080 +12595,3,118000020 +12596,3,118000030 +12597,3,118000040 +12598,4,101000090 +12599,4,101000100 +12600,4,101000110 +12601,4,109000100 +12602,4,105000100 +12603,4,105000110 +12604,4,108000090 +12605,4,110000090 +12606,4,102000100 +12607,4,102000110 +12608,4,106000090 +12609,4,109000090 +12610,4,107000100 +12611,4,103000090 +12612,4,102000090 +12613,4,103000100 +12614,4,106000100 +12615,4,106000110 +12616,4,104000090 +12617,4,104000100 +12618,4,104000110 +12619,4,107000090 +12620,4,104000180 +12621,4,111000040 +12622,4,112000040 +12623,4,108000100 +12624,4,105000090 +12625,4,110000100 +12626,4,118000050 +12627,4,118000060 +12628,5,101000120 +12629,5,109000110 +12630,5,105000120 +12631,5,102000120 +12632,5,107000110 +12633,5,103000120 +12634,5,106000120 +12635,5,104000120 +12636,5,104000190 +12637,5,111000060 +12638,5,112000060 +12639,5,108000110 +12640,5,110000110 +12641,5,118000070 +12642,1,201000010 +12643,1,292000010 +12644,1,299000040 +12645,1,299000070 +12646,1,299000110 +12647,1,299000120 +12648,1,299000140 +12649,2,202000010 +12650,2,290000010 +12651,2,299000010 +12652,2,299000150 +12653,2,299000190 +12654,2,299000200 +12655,2,299000210 +12656,3,298000050 +12657,3,298000060 +12658,3,299000060 +12659,3,299000170 +12660,3,290000120 +12661,3,291000050 +12662,3,292000020 +12663,4,299000670 +12664,4,299000680 +12665,4,204000010 +12666,4,209000040 +12667,4,202000070 +12668,4,209000070 +12669,4,203000110 +12670,5,297000100 +12671,5,291000020 +12672,5,297000130 +12673,5,297000140 +12674,5,203000010 +12675,5,206000030 +12676,5,203000050 +12677,5,202000090 +12678,5,204000080 +12679,3,170004 +12680,3,180004 +12681,4,140000 +12682,5,150010 +12683,5,150020 +12684,5,150030 +12685,5,150040 +12687,3,101000050 +12688,3,101000060 +12689,3,101000080 +12690,3,101000040 +12691,3,109000060 +12692,3,109000070 +12693,3,109000080 +12694,3,105000060 +12695,3,105000070 +12696,3,105000080 +12697,3,104000050 +12698,3,106000050 +12699,3,102000060 +12700,3,102000070 +12701,3,102000080 +12702,3,103000050 +12703,3,105000050 +12704,3,107000060 +12705,3,107000070 +12706,3,107000080 +12707,3,108000050 +12708,3,109000050 +12709,3,103000060 +12710,3,103000070 +12711,3,103000080 +12712,3,110000050 +12713,3,106000060 +12714,3,106000070 +12715,3,106000080 +12716,3,101000070 +12717,3,110000060 +12718,3,104000060 +12719,3,104000070 +12720,3,104000080 +12721,3,102000050 +12722,3,104000170 +12723,3,104000260 +12724,3,111000010 +12725,3,111000020 +12726,3,111000030 +12727,3,112000020 +12728,3,112000030 +12729,3,108000060 +12730,3,108000070 +12731,3,108000080 +12732,3,107000050 +12733,3,112000010 +12734,3,110000070 +12735,3,110000080 +12736,3,118000020 +12737,3,118000030 +12738,3,118000040 +12739,4,101000090 +12740,4,101000100 +12741,4,101000110 +12742,4,109000100 +12743,4,105000100 +12744,4,105000110 +12745,4,108000090 +12746,4,110000090 +12747,4,102000100 +12748,4,102000110 +12749,4,106000090 +12750,4,109000090 +12751,4,107000100 +12752,4,103000090 +12753,4,102000090 +12754,4,103000100 +12755,4,106000100 +12756,4,106000110 +12757,4,104000090 +12758,4,104000100 +12759,4,104000110 +12760,4,107000090 +12761,4,104000180 +12762,4,111000040 +12763,4,112000040 +12764,4,108000100 +12765,4,105000090 +12766,4,110000100 +12767,4,118000050 +12768,4,118000060 +12769,5,101000120 +12770,5,109000110 +12771,5,105000120 +12772,5,102000120 +12773,5,107000110 +12774,5,103000120 +12775,5,106000120 +12776,5,104000120 +12777,5,104000190 +12778,5,111000060 +12779,5,112000060 +12780,5,108000110 +12781,5,110000110 +12782,5,118000070 +12783,1,201000010 +12784,1,292000010 +12785,1,299000040 +12786,1,299000070 +12787,1,299000110 +12788,1,299000120 +12789,1,299000140 +12790,2,202000010 +12791,2,290000010 +12792,2,299000010 +12793,2,299000150 +12794,2,299000190 +12795,2,299000200 +12796,2,299000210 +12797,3,298000050 +12798,3,298000060 +12799,3,299000060 +12800,3,299000170 +12801,3,290000120 +12802,3,291000050 +12803,3,292000020 +12804,4,299000670 +12805,4,299000680 +12806,4,204000010 +12807,4,209000040 +12808,4,202000070 +12809,4,209000070 +12810,4,203000110 +12811,5,297000100 +12812,5,291000020 +12813,5,297000130 +12814,5,297000140 +12815,5,203000010 +12816,5,206000030 +12817,5,203000050 +12818,5,202000090 +12819,5,204000080 +12820,3,170004 +12821,3,180004 +12822,4,140000 +12823,5,150010 +12824,5,150020 +12825,5,150030 +12826,5,150040 +12828,3,101000050 +12829,3,101000060 +12830,3,101000080 +12831,3,101000040 +12832,3,109000060 +12833,3,109000070 +12834,3,109000080 +12835,3,105000060 +12836,3,105000070 +12837,3,105000080 +12838,3,104000050 +12839,3,106000050 +12840,3,102000060 +12841,3,102000070 +12842,3,102000080 +12843,3,103000050 +12844,3,105000050 +12845,3,107000060 +12846,3,107000070 +12847,3,107000080 +12848,3,108000050 +12849,3,109000050 +12850,3,103000060 +12851,3,103000070 +12852,3,103000080 +12853,3,110000050 +12854,3,106000060 +12855,3,106000070 +12856,3,106000080 +12857,3,101000070 +12858,3,110000060 +12859,3,104000060 +12860,3,104000070 +12861,3,104000080 +12862,3,102000050 +12863,3,104000170 +12864,3,104000260 +12865,3,111000010 +12866,3,111000020 +12867,3,111000030 +12868,3,112000020 +12869,3,112000030 +12870,3,108000060 +12871,3,108000070 +12872,3,108000080 +12873,3,107000050 +12874,3,112000010 +12875,3,110000070 +12876,3,110000080 +12877,3,118000020 +12878,3,118000030 +12879,3,118000040 +12880,4,101000090 +12881,4,101000100 +12882,4,101000110 +12883,4,109000100 +12884,4,105000100 +12885,4,105000110 +12886,4,108000090 +12887,4,110000090 +12888,4,102000100 +12889,4,102000110 +12890,4,106000090 +12891,4,109000090 +12892,4,107000100 +12893,4,103000090 +12894,4,102000090 +12895,4,103000100 +12896,4,106000100 +12897,4,106000110 +12898,4,104000090 +12899,4,104000100 +12900,4,104000110 +12901,4,107000090 +12902,4,104000180 +12903,4,111000040 +12904,4,112000040 +12905,4,108000100 +12906,4,105000090 +12907,4,110000100 +12908,4,118000050 +12909,4,118000060 +12910,5,101000120 +12911,5,109000110 +12912,5,105000120 +12913,5,102000120 +12914,5,107000110 +12915,5,103000120 +12916,5,106000120 +12917,5,104000120 +12918,5,104000190 +12919,5,111000060 +12920,5,112000060 +12921,5,108000110 +12922,5,110000110 +12923,5,118000070 +12924,1,201000010 +12925,1,292000010 +12926,1,299000040 +12927,1,299000070 +12928,1,299000110 +12929,1,299000120 +12930,1,299000140 +12931,2,202000010 +12932,2,290000010 +12933,2,299000010 +12934,2,299000150 +12935,2,299000190 +12936,2,299000200 +12937,2,299000210 +12938,3,298000050 +12939,3,298000060 +12940,3,299000060 +12941,3,299000170 +12942,3,290000120 +12943,3,291000050 +12944,3,292000020 +12945,4,299000670 +12946,4,299000680 +12947,4,204000010 +12948,4,209000040 +12949,4,202000070 +12950,4,209000070 +12951,4,203000110 +12952,5,297000100 +12953,5,291000020 +12954,5,297000130 +12955,5,297000140 +12956,5,203000010 +12957,5,206000030 +12958,5,203000050 +12959,5,202000090 +12960,5,204000080 +12961,3,170004 +12962,3,180004 +12963,4,140000 +12964,5,150010 +12965,5,150020 +12966,5,150030 +12967,5,150040 +12968,5,190000 +12969,5,200000 +12970,5,210000 +12972,3,102000060 +12973,3,102000070 +12974,3,102000080 +12975,3,103000050 +12976,3,105000050 +12977,3,107000060 +12978,3,107000070 +12979,3,107000080 +12980,3,108000050 +12981,3,109000050 +12982,3,103000060 +12983,3,103000070 +12984,3,103000080 +12985,3,110000050 +12986,4,102000100 +12987,4,102000110 +12988,4,102000350 +12989,4,106000090 +12990,4,109000090 +12991,4,107000100 +12992,4,103000090 +12993,4,102000090 +12994,4,103000100 +12995,5,102000120 +12996,5,107000110 +12997,5,103000120 +12998,1,102000000 +12999,2,102000001 +13000,3,102000002 +13001,4,102000003 +13002,5,102000004 +13003,1,105000000 +13004,2,105000001 +13005,3,105000002 +13006,4,105000003 +13007,5,105000004 +13008,1,112000000 +13009,2,112000001 +13010,3,112000002 +13011,4,112000003 +13012,5,112000004 +13013,3,170004 +13014,4,170005 +13015,3,180004 +13016,4,180005 +13017,4,140000 +13018,4,150010 +13019,4,150020 +13020,4,150030 +13021,4,150040 +13023,3,118000020 +13024,3,118000030 +13025,3,118000040 +13026,3,106000060 +13027,3,106000070 +13028,3,106000080 +13029,3,101000070 +13030,3,110000060 +13031,3,104000060 +13032,3,104000070 +13033,3,104000080 +13034,3,102000050 +13035,3,104000170 +13036,3,104000260 +13037,4,118000050 +13038,4,118000060 +13039,4,106000100 +13040,4,106000110 +13041,4,104000090 +13042,4,104000100 +13043,4,104000110 +13044,4,104000270 +13045,4,107000090 +13046,4,104000180 +13047,5,118000070 +13048,5,106000120 +13049,5,104000120 +13050,5,104000190 +13051,1,103000000 +13052,2,103000001 +13053,3,103000002 +13054,4,103000003 +13055,5,103000004 +13056,1,111000000 +13057,2,111000001 +13058,3,111000002 +13059,4,111000003 +13060,5,111000004 +13061,1,115000000 +13062,2,115000001 +13063,3,115000002 +13064,4,115000003 +13065,5,115000004 +13066,4,110024 +13067,4,110034 +13068,4,110044 +13069,4,110054 +13070,3,110060 +13071,3,110070 +13072,3,110080 +13073,3,110090 +13074,3,110100 +13075,3,110110 +13076,3,110120 +13077,3,110130 +13078,3,110140 +13079,3,110150 +13080,3,110160 +13081,3,110170 +13082,3,110180 +13083,3,110190 +13084,3,110200 +13085,3,110210 +13086,3,110220 +13087,3,110230 +13088,3,110240 +13089,3,110250 +13090,3,110260 +13091,3,110270 +13092,3,110620 +13093,3,110670 +13094,4,140000 +13095,4,150010 +13096,4,150020 +13097,4,150030 +13098,4,150040 +13100,3,111000010 +13101,3,111000020 +13102,3,111000030 +13103,3,112000020 +13104,3,112000030 +13105,3,108000060 +13106,3,108000070 +13107,3,108000080 +13108,3,107000050 +13109,3,112000010 +13110,3,110000070 +13111,3,110000080 +13112,4,111000040 +13113,4,112000040 +13114,4,108000100 +13115,4,105000090 +13116,4,110000100 +13117,5,111000060 +13118,5,112000060 +13119,5,108000110 +13120,5,110000110 +13121,1,108000000 +13122,2,108000001 +13123,3,108000002 +13124,4,108000003 +13125,5,108000004 +13126,1,107000000 +13127,2,107000001 +13128,3,107000002 +13129,4,107000003 +13130,5,107000004 +13131,1,120000000 +13132,2,120000001 +13133,3,120000002 +13134,4,120000003 +13135,5,120000004 +13136,4,120024 +13137,4,120034 +13138,4,120044 +13139,4,120054 +13140,3,120241 +13141,3,120251 +13142,3,120261 +13143,3,120271 +13144,3,120300 +13145,3,120310 +13146,3,120320 +13147,3,120330 +13148,3,120340 +13149,3,120350 +13150,3,120360 +13151,3,120370 +13152,3,120380 +13153,3,120390 +13154,3,120400 +13155,3,120410 +13156,3,120420 +13157,3,120430 +13158,3,120450 +13159,3,120460 +13160,3,120550 +13161,3,120560 +13162,3,120570 +13163,3,120990 +13164,3,121000 +13165,3,121010 +13166,3,121020 +13167,3,121100 +13168,4,140000 +13169,4,150010 +13170,4,150020 +13171,4,150030 +13172,4,150040 +13174,3,101000050 +13175,3,101000060 +13176,3,101000080 +13177,3,101000040 +13178,3,109000060 +13179,3,109000070 +13180,3,109000080 +13181,3,105000060 +13182,3,105000070 +13183,3,105000080 +13184,3,104000050 +13185,3,106000050 +13186,4,101000090 +13187,4,101000100 +13188,4,101000110 +13189,4,109000100 +13190,4,105000100 +13191,4,105000110 +13192,4,108000090 +13193,4,110000090 +13194,5,101000120 +13195,5,109000110 +13196,5,105000120 +13197,1,101000000 +13198,2,101000001 +13199,3,101000002 +13200,4,101000008 +13201,5,101000004 +13202,1,109000000 +13203,2,109000001 +13204,3,109000002 +13205,4,109000003 +13206,5,109000004 +13207,4,130024 +13208,4,130034 +13209,4,130044 +13210,4,130054 +13211,3,130060 +13212,3,130070 +13213,3,130080 +13214,3,130090 +13215,3,130100 +13216,3,130110 +13217,3,130120 +13218,3,130130 +13219,3,130140 +13220,3,130150 +13221,3,130160 +13222,3,130170 +13223,3,130180 +13224,3,130190 +13225,3,130200 +13226,3,130420 +13227,3,130510 +13228,3,130520 +13229,3,130531 +13230,3,130540 +13231,3,130660 +13232,3,130700 +13233,3,130790 +13234,3,130800 +13235,3,131130 +13236,4,140000 +13237,4,150010 +13238,4,150020 +13239,4,150030 +13240,4,150040 +13242,3,101000050 +13243,3,101000060 +13244,3,101000080 +13245,3,101000040 +13246,3,109000060 +13247,3,109000070 +13248,3,109000080 +13249,3,105000060 +13250,3,105000070 +13251,3,105000080 +13252,3,104000050 +13253,3,106000050 +13254,4,101000090 +13255,4,101000100 +13256,4,101000110 +13257,4,109000100 +13258,4,105000100 +13259,4,105000110 +13260,4,108000090 +13261,4,110000090 +13262,5,101000120 +13263,5,109000110 +13264,5,105000120 +13265,1,101000000 +13266,2,101000001 +13267,3,101000002 +13268,4,101000008 +13269,5,101000004 +13270,1,109000000 +13271,2,109000001 +13272,3,109000002 +13273,4,109000003 +13274,5,109000004 +13275,3,170004 +13276,4,170005 +13277,3,180004 +13278,4,180005 +13279,4,140000 +13280,4,150010 +13281,4,150020 +13282,4,150030 +13283,4,150040 +13285,3,102000060 +13286,3,102000070 +13287,3,102000080 +13288,3,103000050 +13289,3,105000050 +13290,3,107000060 +13291,3,107000070 +13292,3,107000080 +13293,3,108000050 +13294,3,109000050 +13295,3,103000060 +13296,3,103000070 +13297,3,103000080 +13298,3,110000050 +13299,4,102000100 +13300,4,102000110 +13301,4,102000350 +13302,4,106000090 +13303,4,109000090 +13304,4,107000100 +13305,4,103000090 +13306,4,102000090 +13307,4,103000100 +13308,5,102000120 +13309,5,107000110 +13310,5,103000120 +13311,1,102000000 +13312,2,102000001 +13313,3,102000002 +13314,4,102000003 +13315,5,102000004 +13316,1,105000000 +13317,2,105000001 +13318,3,105000002 +13319,4,105000003 +13320,5,105000004 +13321,1,112000000 +13322,2,112000001 +13323,3,112000002 +13324,4,112000003 +13325,5,112000004 +13326,4,110024 +13327,4,110034 +13328,4,110044 +13329,4,110054 +13330,3,110060 +13331,3,110070 +13332,3,110080 +13333,3,110090 +13334,3,110100 +13335,3,110110 +13336,3,110120 +13337,3,110130 +13338,3,110140 +13339,3,110150 +13340,3,110160 +13341,3,110170 +13342,3,110180 +13343,3,110190 +13344,3,110200 +13345,3,110210 +13346,3,110220 +13347,3,110230 +13348,3,110240 +13349,3,110250 +13350,3,110260 +13351,3,110270 +13352,3,110620 +13353,3,110670 +13354,4,140000 +13355,4,150010 +13356,4,150020 +13357,4,150030 +13358,4,150040 +13360,3,118000020 +13361,3,118000030 +13362,3,118000040 +13363,3,106000060 +13364,3,106000070 +13365,3,106000080 +13366,3,101000070 +13367,3,110000060 +13368,3,104000060 +13369,3,104000070 +13370,3,104000080 +13371,3,102000050 +13372,3,104000170 +13373,3,104000260 +13374,4,118000050 +13375,4,118000060 +13376,4,106000100 +13377,4,106000110 +13378,4,104000090 +13379,4,104000100 +13380,4,104000110 +13381,4,104000270 +13382,4,107000090 +13383,4,104000180 +13384,5,118000070 +13385,5,106000120 +13386,5,104000120 +13387,5,104000190 +13388,1,103000000 +13389,2,103000001 +13390,3,103000002 +13391,4,103000003 +13392,5,103000004 +13393,1,111000000 +13394,2,111000001 +13395,3,111000002 +13396,4,111000003 +13397,5,111000004 +13398,1,115000000 +13399,2,115000001 +13400,3,115000002 +13401,4,115000003 +13402,5,115000004 +13403,4,120024 +13404,4,120034 +13405,4,120044 +13406,4,120054 +13407,3,120241 +13408,3,120251 +13409,3,120261 +13410,3,120271 +13411,3,120300 +13412,3,120310 +13413,3,120320 +13414,3,120330 +13415,3,120340 +13416,3,120350 +13417,3,120360 +13418,3,120370 +13419,3,120380 +13420,3,120390 +13421,3,120400 +13422,3,120410 +13423,3,120420 +13424,3,120430 +13425,3,120450 +13426,3,120460 +13427,3,120550 +13428,3,120560 +13429,3,120570 +13430,3,120990 +13431,3,121000 +13432,3,121010 +13433,3,121020 +13434,3,121100 +13435,4,140000 +13436,4,150010 +13437,4,150020 +13438,4,150030 +13439,4,150040 +13441,3,111000010 +13442,3,111000020 +13443,3,111000030 +13444,3,112000020 +13445,3,112000030 +13446,3,108000060 +13447,3,108000070 +13448,3,108000080 +13449,3,107000050 +13450,3,112000010 +13451,3,110000070 +13452,3,110000080 +13453,4,111000040 +13454,4,112000040 +13455,4,108000100 +13456,4,105000090 +13457,4,110000100 +13458,5,111000060 +13459,5,112000060 +13460,5,108000110 +13461,5,110000110 +13462,1,108000000 +13463,2,108000001 +13464,3,108000002 +13465,4,108000003 +13466,5,108000004 +13467,1,107000000 +13468,2,107000001 +13469,3,107000002 +13470,4,107000003 +13471,5,107000004 +13472,1,120000000 +13473,2,120000001 +13474,3,120000002 +13475,4,120000003 +13476,5,120000004 +13477,4,130024 +13478,4,130034 +13479,4,130044 +13480,4,130054 +13481,3,130060 +13482,3,130070 +13483,3,130080 +13484,3,130090 +13485,3,130100 +13486,3,130110 +13487,3,130120 +13488,3,130130 +13489,3,130140 +13490,3,130150 +13491,3,130160 +13492,3,130170 +13493,3,130180 +13494,3,130190 +13495,3,130200 +13496,3,130420 +13497,3,130510 +13498,3,130520 +13499,3,130531 +13500,3,130540 +13501,3,130660 +13502,3,130700 +13503,3,130790 +13504,3,130800 +13505,3,131130 +13506,4,140000 +13507,4,150010 +13508,4,150020 +13509,4,150030 +13510,4,150040 +13512,1,101000010 +13513,1,102000010 +13514,1,103000010 +13515,1,104000010 +13516,1,105000010 +13517,1,106000010 +13518,1,107000010 +13519,1,108000010 +13520,1,109000010 +13521,1,110000010 +13522,2,101000020 +13523,2,101000030 +13524,2,102000020 +13525,2,102000030 +13526,2,102000040 +13527,2,103000020 +13528,2,103000030 +13529,2,103000040 +13530,2,104000020 +13531,2,104000030 +13532,2,104000040 +13533,2,105000020 +13534,2,105000030 +13535,2,105000040 +13536,2,106000020 +13537,2,106000030 +13538,2,106000040 +13539,2,107000020 +13540,2,107000030 +13541,2,107000040 +13542,2,108000020 +13543,2,108000030 +13544,2,108000040 +13545,2,109000020 +13546,2,109000030 +13547,2,109000040 +13548,2,110000020 +13549,2,110000030 +13550,2,110000040 +13551,2,118000010 +13552,3,101000050 +13553,3,101000060 +13554,3,101000080 +13555,3,101000040 +13556,3,109000060 +13557,3,109000070 +13558,3,109000080 +13559,3,105000060 +13560,3,105000070 +13561,3,105000080 +13562,3,104000050 +13563,3,106000050 +13564,3,102000060 +13565,3,102000070 +13566,3,102000080 +13567,3,103000050 +13568,3,105000050 +13569,3,107000060 +13570,3,107000070 +13571,3,107000080 +13572,3,108000050 +13573,3,109000050 +13574,3,103000060 +13575,3,103000070 +13576,3,103000080 +13577,3,110000050 +13578,3,106000060 +13579,3,106000070 +13580,3,106000080 +13581,3,101000070 +13582,3,110000060 +13583,3,104000060 +13584,3,104000070 +13585,3,104000080 +13586,3,102000050 +13587,3,104000170 +13588,3,104000260 +13589,3,111000010 +13590,3,111000020 +13591,3,111000030 +13592,3,112000020 +13593,3,112000030 +13594,3,108000060 +13595,3,108000070 +13596,3,108000080 +13597,3,107000050 +13598,3,112000010 +13599,3,110000070 +13600,3,110000080 +13601,3,118000020 +13602,3,118000030 +13603,3,118000040 +13604,4,101000090 +13605,4,101000100 +13606,4,101000110 +13607,4,109000100 +13608,4,105000100 +13609,4,105000110 +13610,4,108000090 +13611,4,110000090 +13612,4,102000100 +13613,4,102000110 +13614,4,106000090 +13615,4,109000090 +13616,4,107000100 +13617,4,103000090 +13618,4,102000090 +13619,4,103000100 +13620,4,106000100 +13621,4,106000110 +13622,4,104000090 +13623,4,104000100 +13624,4,104000110 +13625,4,107000090 +13626,4,104000180 +13627,4,111000040 +13628,4,112000040 +13629,4,108000100 +13630,4,105000090 +13631,4,110000100 +13632,4,118000050 +13633,4,118000060 +13634,5,101000120 +13635,5,109000110 +13636,5,105000120 +13637,5,102000120 +13638,5,107000110 +13639,5,103000120 +13640,5,106000120 +13641,5,104000120 +13642,5,104000190 +13643,5,111000060 +13644,5,112000060 +13645,5,108000110 +13646,5,110000110 +13647,5,118000070 +13648,1,201000010 +13649,1,292000010 +13650,1,299000040 +13651,1,299000070 +13652,1,299000110 +13653,1,299000120 +13654,1,299000140 +13655,2,202000010 +13656,2,290000010 +13657,2,299000010 +13658,2,299000150 +13659,2,299000190 +13660,2,299000200 +13661,2,299000210 +13662,3,298000050 +13663,3,298000060 +13664,3,299000060 +13665,3,299000170 +13666,3,290000120 +13667,3,291000050 +13668,3,292000020 +13669,4,299000670 +13670,4,299000680 +13671,4,204000010 +13672,4,209000040 +13673,4,202000070 +13674,4,209000070 +13675,4,203000110 +13676,4,290000110 +13677,5,297000100 +13678,5,291000020 +13679,5,297000130 +13680,5,297000140 +13681,5,203000010 +13682,5,206000030 +13683,5,203000050 +13684,5,202000090 +13685,5,204000080 +13686,5,202000150 +13687,1,170002 +13688,1,180002 +13689,2,170003 +13690,2,180003 +13691,3,170004 +13692,3,180004 +13693,4,140000 +13694,5,150010 +13695,5,150020 +13696,5,150030 +13697,5,150040 +13699,1,101000010 +13700,1,102000010 +13701,1,103000010 +13702,1,104000010 +13703,1,105000010 +13704,1,106000010 +13705,1,107000010 +13706,1,108000010 +13707,1,109000010 +13708,1,110000010 +13709,2,101000020 +13710,2,101000030 +13711,2,102000020 +13712,2,102000030 +13713,2,102000040 +13714,2,103000020 +13715,2,103000030 +13716,2,103000040 +13717,2,104000020 +13718,2,104000030 +13719,2,104000040 +13720,2,105000020 +13721,2,105000030 +13722,2,105000040 +13723,2,106000020 +13724,2,106000030 +13725,2,106000040 +13726,2,107000020 +13727,2,107000030 +13728,2,107000040 +13729,2,108000020 +13730,2,108000030 +13731,2,108000040 +13732,2,109000020 +13733,2,109000030 +13734,2,109000040 +13735,2,110000020 +13736,2,110000030 +13737,2,110000040 +13738,2,118000010 +13739,3,101000050 +13740,3,101000060 +13741,3,101000080 +13742,3,101000040 +13743,3,109000060 +13744,3,109000070 +13745,3,109000080 +13746,3,105000060 +13747,3,105000070 +13748,3,105000080 +13749,3,104000050 +13750,3,106000050 +13751,3,102000060 +13752,3,102000070 +13753,3,102000080 +13754,3,103000050 +13755,3,105000050 +13756,3,107000060 +13757,3,107000070 +13758,3,107000080 +13759,3,108000050 +13760,3,109000050 +13761,3,103000060 +13762,3,103000070 +13763,3,103000080 +13764,3,110000050 +13765,3,106000060 +13766,3,106000070 +13767,3,106000080 +13768,3,101000070 +13769,3,110000060 +13770,3,104000060 +13771,3,104000070 +13772,3,104000080 +13773,3,102000050 +13774,3,104000170 +13775,3,104000260 +13776,3,111000010 +13777,3,111000020 +13778,3,111000030 +13779,3,112000020 +13780,3,112000030 +13781,3,108000060 +13782,3,108000070 +13783,3,108000080 +13784,3,107000050 +13785,3,112000010 +13786,3,110000070 +13787,3,110000080 +13788,3,118000020 +13789,3,118000030 +13790,3,118000040 +13791,4,101000090 +13792,4,101000100 +13793,4,101000110 +13794,4,109000100 +13795,4,105000100 +13796,4,105000110 +13797,4,108000090 +13798,4,110000090 +13799,4,102000100 +13800,4,102000110 +13801,4,106000090 +13802,4,109000090 +13803,4,107000100 +13804,4,103000090 +13805,4,102000090 +13806,4,103000100 +13807,4,106000100 +13808,4,106000110 +13809,4,104000090 +13810,4,104000100 +13811,4,104000110 +13812,4,107000090 +13813,4,104000180 +13814,4,111000040 +13815,4,112000040 +13816,4,108000100 +13817,4,105000090 +13818,4,110000100 +13819,4,118000050 +13820,4,118000060 +13821,5,101000120 +13822,5,109000110 +13823,5,105000120 +13824,5,102000120 +13825,5,107000110 +13826,5,103000120 +13827,5,106000120 +13828,5,104000120 +13829,5,104000190 +13830,5,111000060 +13831,5,112000060 +13832,5,108000110 +13833,5,110000110 +13834,5,118000070 +13835,1,201000010 +13836,1,292000010 +13837,1,299000040 +13838,1,299000070 +13839,1,299000110 +13840,1,299000120 +13841,1,299000140 +13842,2,202000010 +13843,2,290000010 +13844,2,299000010 +13845,2,299000150 +13846,2,299000190 +13847,2,299000200 +13848,2,299000210 +13849,3,298000050 +13850,3,298000060 +13851,3,299000060 +13852,3,299000170 +13853,3,290000120 +13854,3,291000050 +13855,3,292000020 +13856,4,299000670 +13857,4,299000680 +13858,4,204000010 +13859,4,209000040 +13860,4,202000070 +13861,4,209000070 +13862,4,203000110 +13863,4,290000110 +13864,5,297000100 +13865,5,291000020 +13866,5,297000130 +13867,5,297000140 +13868,5,203000010 +13869,5,206000030 +13870,5,203000050 +13871,5,202000090 +13872,5,204000080 +13873,5,202000150 +13874,2,170003 +13875,2,180003 +13876,3,170004 +13877,3,180004 +13878,4,140000 +13879,5,150010 +13880,5,150020 +13881,5,150030 +13882,5,150040 +13884,3,101000050 +13885,3,101000060 +13886,3,101000080 +13887,3,101000040 +13888,3,109000060 +13889,3,109000070 +13890,3,109000080 +13891,3,105000060 +13892,3,105000070 +13893,3,105000080 +13894,3,104000050 +13895,3,106000050 +13896,3,102000060 +13897,3,102000070 +13898,3,102000080 +13899,3,103000050 +13900,3,105000050 +13901,3,107000060 +13902,3,107000070 +13903,3,107000080 +13904,3,108000050 +13905,3,109000050 +13906,3,103000060 +13907,3,103000070 +13908,3,103000080 +13909,3,110000050 +13910,3,106000060 +13911,3,106000070 +13912,3,106000080 +13913,3,101000070 +13914,3,110000060 +13915,3,104000060 +13916,3,104000070 +13917,3,104000080 +13918,3,102000050 +13919,3,104000170 +13920,3,104000260 +13921,3,111000010 +13922,3,111000020 +13923,3,111000030 +13924,3,112000020 +13925,3,112000030 +13926,3,108000060 +13927,3,108000070 +13928,3,108000080 +13929,3,107000050 +13930,3,112000010 +13931,3,110000070 +13932,3,110000080 +13933,3,118000020 +13934,3,118000030 +13935,3,118000040 +13936,4,101000090 +13937,4,101000100 +13938,4,101000110 +13939,4,109000100 +13940,4,105000100 +13941,4,105000110 +13942,4,108000090 +13943,4,110000090 +13944,4,102000100 +13945,4,102000110 +13946,4,106000090 +13947,4,109000090 +13948,4,107000100 +13949,4,103000090 +13950,4,102000090 +13951,4,103000100 +13952,4,106000100 +13953,4,106000110 +13954,4,104000090 +13955,4,104000100 +13956,4,104000110 +13957,4,107000090 +13958,4,104000180 +13959,4,111000040 +13960,4,112000040 +13961,4,108000100 +13962,4,105000090 +13963,4,110000100 +13964,4,118000050 +13965,4,118000060 +13966,5,101000120 +13967,5,109000110 +13968,5,105000120 +13969,5,102000120 +13970,5,107000110 +13971,5,103000120 +13972,5,106000120 +13973,5,104000120 +13974,5,104000190 +13975,5,111000060 +13976,5,112000060 +13977,5,108000110 +13978,5,110000110 +13979,5,118000070 +13980,1,201000010 +13981,1,292000010 +13982,1,299000040 +13983,1,299000070 +13984,1,299000110 +13985,1,299000120 +13986,1,299000140 +13987,2,202000010 +13988,2,290000010 +13989,2,299000010 +13990,2,299000150 +13991,2,299000190 +13992,2,299000200 +13993,2,299000210 +13994,3,298000050 +13995,3,298000060 +13996,3,299000060 +13997,3,299000170 +13998,3,290000120 +13999,3,291000050 +14000,3,292000020 +14001,4,299000670 +14002,4,299000680 +14003,4,204000010 +14004,4,209000040 +14005,4,202000070 +14006,4,209000070 +14007,4,203000110 +14008,4,290000110 +14009,5,297000100 +14010,5,291000020 +14011,5,297000130 +14012,5,297000140 +14013,5,203000010 +14014,5,206000030 +14015,5,203000050 +14016,5,202000090 +14017,5,204000080 +14018,5,202000150 +14019,3,170004 +14020,3,180004 +14021,4,140000 +14022,5,150010 +14023,5,150020 +14024,5,150030 +14025,5,150040 +14027,3,101000050 +14028,3,101000060 +14029,3,101000080 +14030,3,101000040 +14031,3,109000060 +14032,3,109000070 +14033,3,109000080 +14034,3,105000060 +14035,3,105000070 +14036,3,105000080 +14037,3,104000050 +14038,3,106000050 +14039,3,102000060 +14040,3,102000070 +14041,3,102000080 +14042,3,103000050 +14043,3,105000050 +14044,3,107000060 +14045,3,107000070 +14046,3,107000080 +14047,3,108000050 +14048,3,109000050 +14049,3,103000060 +14050,3,103000070 +14051,3,103000080 +14052,3,110000050 +14053,3,106000060 +14054,3,106000070 +14055,3,106000080 +14056,3,101000070 +14057,3,110000060 +14058,3,104000060 +14059,3,104000070 +14060,3,104000080 +14061,3,102000050 +14062,3,104000170 +14063,3,104000260 +14064,3,111000010 +14065,3,111000020 +14066,3,111000030 +14067,3,112000020 +14068,3,112000030 +14069,3,108000060 +14070,3,108000070 +14071,3,108000080 +14072,3,107000050 +14073,3,112000010 +14074,3,110000070 +14075,3,110000080 +14076,3,118000020 +14077,3,118000030 +14078,3,118000040 +14079,4,101000090 +14080,4,101000100 +14081,4,101000110 +14082,4,109000100 +14083,4,105000100 +14084,4,105000110 +14085,4,108000090 +14086,4,110000090 +14087,4,102000100 +14088,4,102000110 +14089,4,106000090 +14090,4,109000090 +14091,4,107000100 +14092,4,103000090 +14093,4,102000090 +14094,4,103000100 +14095,4,106000100 +14096,4,106000110 +14097,4,104000090 +14098,4,104000100 +14099,4,104000110 +14100,4,107000090 +14101,4,104000180 +14102,4,111000040 +14103,4,112000040 +14104,4,108000100 +14105,4,105000090 +14106,4,110000100 +14107,4,118000050 +14108,4,118000060 +14109,5,101000120 +14110,5,109000110 +14111,5,105000120 +14112,5,102000120 +14113,5,107000110 +14114,5,103000120 +14115,5,106000120 +14116,5,104000120 +14117,5,104000190 +14118,5,111000060 +14119,5,112000060 +14120,5,108000110 +14121,5,110000110 +14122,5,118000070 +14123,1,201000010 +14124,1,292000010 +14125,1,299000040 +14126,1,299000070 +14127,1,299000110 +14128,1,299000120 +14129,1,299000140 +14130,2,202000010 +14131,2,290000010 +14132,2,299000010 +14133,2,299000150 +14134,2,299000190 +14135,2,299000200 +14136,2,299000210 +14137,3,298000050 +14138,3,298000060 +14139,3,299000060 +14140,3,299000170 +14141,3,290000120 +14142,3,291000050 +14143,3,292000020 +14144,4,299000670 +14145,4,299000680 +14146,4,204000010 +14147,4,209000040 +14148,4,202000070 +14149,4,209000070 +14150,4,203000110 +14151,4,290000110 +14152,5,297000100 +14153,5,291000020 +14154,5,297000130 +14155,5,297000140 +14156,5,203000010 +14157,5,206000030 +14158,5,203000050 +14159,5,202000090 +14160,5,204000080 +14161,5,202000150 +14162,3,170004 +14163,3,180004 +14164,4,140000 +14165,5,150010 +14166,5,150020 +14167,5,150030 +14168,5,150040 +14170,3,101000050 +14171,3,101000060 +14172,3,101000080 +14173,3,101000040 +14174,3,109000060 +14175,3,109000070 +14176,3,109000080 +14177,3,105000060 +14178,3,105000070 +14179,3,105000080 +14180,3,104000050 +14181,3,106000050 +14182,3,102000060 +14183,3,102000070 +14184,3,102000080 +14185,3,103000050 +14186,3,105000050 +14187,3,107000060 +14188,3,107000070 +14189,3,107000080 +14190,3,108000050 +14191,3,109000050 +14192,3,103000060 +14193,3,103000070 +14194,3,103000080 +14195,3,110000050 +14196,3,106000060 +14197,3,106000070 +14198,3,106000080 +14199,3,101000070 +14200,3,110000060 +14201,3,104000060 +14202,3,104000070 +14203,3,104000080 +14204,3,102000050 +14205,3,104000170 +14206,3,104000260 +14207,3,111000010 +14208,3,111000020 +14209,3,111000030 +14210,3,112000020 +14211,3,112000030 +14212,3,108000060 +14213,3,108000070 +14214,3,108000080 +14215,3,107000050 +14216,3,112000010 +14217,3,110000070 +14218,3,110000080 +14219,3,118000020 +14220,3,118000030 +14221,3,118000040 +14222,4,101000090 +14223,4,101000100 +14224,4,101000110 +14225,4,109000100 +14226,4,105000100 +14227,4,105000110 +14228,4,108000090 +14229,4,110000090 +14230,4,102000100 +14231,4,102000110 +14232,4,106000090 +14233,4,109000090 +14234,4,107000100 +14235,4,103000090 +14236,4,102000090 +14237,4,103000100 +14238,4,106000100 +14239,4,106000110 +14240,4,104000090 +14241,4,104000100 +14242,4,104000110 +14243,4,107000090 +14244,4,104000180 +14245,4,111000040 +14246,4,112000040 +14247,4,108000100 +14248,4,105000090 +14249,4,110000100 +14250,4,118000050 +14251,4,118000060 +14252,5,101000120 +14253,5,109000110 +14254,5,105000120 +14255,5,102000120 +14256,5,107000110 +14257,5,103000120 +14258,5,106000120 +14259,5,104000120 +14260,5,104000190 +14261,5,111000060 +14262,5,112000060 +14263,5,108000110 +14264,5,110000110 +14265,5,118000070 +14266,1,201000010 +14267,1,292000010 +14268,1,299000040 +14269,1,299000070 +14270,1,299000110 +14271,1,299000120 +14272,1,299000140 +14273,2,202000010 +14274,2,290000010 +14275,2,299000010 +14276,2,299000150 +14277,2,299000190 +14278,2,299000200 +14279,2,299000210 +14280,3,298000050 +14281,3,298000060 +14282,3,299000060 +14283,3,299000170 +14284,3,290000120 +14285,3,291000050 +14286,3,292000020 +14287,4,299000670 +14288,4,299000680 +14289,4,204000010 +14290,4,209000040 +14291,4,202000070 +14292,4,209000070 +14293,4,203000110 +14294,4,290000110 +14295,5,297000100 +14296,5,291000020 +14297,5,297000130 +14298,5,297000140 +14299,5,203000010 +14300,5,206000030 +14301,5,203000050 +14302,5,202000090 +14303,5,204000080 +14304,5,202000150 +14305,3,170004 +14306,3,180004 +14307,4,140000 +14308,5,150010 +14309,5,150020 +14310,5,150030 +14311,5,150040 +14312,5,190000 +14313,5,200000 +14314,5,210000 +14316,1,101000010 +14317,1,102000010 +14318,1,103000010 +14319,1,104000010 +14320,1,105000010 +14321,1,106000010 +14322,1,107000010 +14323,1,108000010 +14324,1,109000010 +14325,1,110000010 +14326,2,101000020 +14327,2,101000030 +14328,2,102000020 +14329,2,102000030 +14330,2,102000040 +14331,2,103000020 +14332,2,103000030 +14333,2,103000040 +14334,2,104000020 +14335,2,104000030 +14336,2,104000040 +14337,2,105000020 +14338,2,105000030 +14339,2,105000040 +14340,2,106000020 +14341,2,106000030 +14342,2,106000040 +14343,2,107000020 +14344,2,107000030 +14345,2,107000040 +14346,2,108000020 +14347,2,108000030 +14348,2,108000040 +14349,2,109000020 +14350,2,109000030 +14351,2,109000040 +14352,2,110000020 +14353,2,110000030 +14354,2,110000040 +14355,2,118000010 +14356,3,101000050 +14357,3,101000060 +14358,3,101000080 +14359,3,101000040 +14360,3,109000060 +14361,3,109000070 +14362,3,109000080 +14363,3,105000060 +14364,3,105000070 +14365,3,105000080 +14366,3,104000050 +14367,3,106000050 +14368,3,102000060 +14369,3,102000070 +14370,3,102000080 +14371,3,103000050 +14372,3,105000050 +14373,3,107000060 +14374,3,107000070 +14375,3,107000080 +14376,3,108000050 +14377,3,109000050 +14378,3,103000060 +14379,3,103000070 +14380,3,103000080 +14381,3,110000050 +14382,3,106000060 +14383,3,106000070 +14384,3,106000080 +14385,3,101000070 +14386,3,110000060 +14387,3,104000060 +14388,3,104000070 +14389,3,104000080 +14390,3,102000050 +14391,3,104000170 +14392,3,104000260 +14393,3,111000010 +14394,3,111000020 +14395,3,111000030 +14396,3,112000020 +14397,3,112000030 +14398,3,108000060 +14399,3,108000070 +14400,3,108000080 +14401,3,107000050 +14402,3,112000010 +14403,3,110000070 +14404,3,110000080 +14405,3,118000020 +14406,3,118000030 +14407,3,118000040 +14408,4,101000090 +14409,4,101000100 +14410,4,101000110 +14411,4,109000100 +14412,4,105000100 +14413,4,105000110 +14414,4,108000090 +14415,4,110000090 +14416,4,102000100 +14417,4,102000110 +14418,4,106000090 +14419,4,109000090 +14420,4,107000100 +14421,4,103000090 +14422,4,102000090 +14423,4,103000100 +14424,4,106000100 +14425,4,106000110 +14426,4,104000090 +14427,4,104000100 +14428,4,104000110 +14429,4,107000090 +14430,4,104000180 +14431,4,111000040 +14432,4,112000040 +14433,4,108000100 +14434,4,105000090 +14435,4,110000100 +14436,4,118000050 +14437,4,118000060 +14438,5,101000120 +14439,5,109000110 +14440,5,105000120 +14441,5,102000120 +14442,5,107000110 +14443,5,103000120 +14444,5,106000120 +14445,5,104000120 +14446,5,104000190 +14447,5,111000060 +14448,5,112000060 +14449,5,108000110 +14450,5,110000110 +14451,5,118000070 +14452,1,201000010 +14453,1,292000010 +14454,1,299000040 +14455,1,299000070 +14456,1,299000110 +14457,1,299000120 +14458,1,299000140 +14459,2,202000010 +14460,2,290000010 +14461,2,299000010 +14462,2,299000150 +14463,2,299000190 +14464,2,299000200 +14465,2,299000210 +14466,3,298000050 +14467,3,298000060 +14468,3,299000060 +14469,3,299000170 +14470,3,290000120 +14471,3,291000050 +14472,3,292000020 +14473,4,299000670 +14474,4,299000680 +14475,4,204000010 +14476,4,209000040 +14477,4,202000070 +14478,4,209000070 +14479,4,203000110 +14480,4,290000110 +14481,4,206000110 +14482,5,297000100 +14483,5,291000020 +14484,5,297000130 +14485,5,297000140 +14486,5,203000010 +14487,5,206000030 +14488,5,203000050 +14489,5,202000090 +14490,5,204000080 +14491,5,202000150 +14492,5,204000100 +14493,1,170002 +14494,1,180002 +14495,2,170003 +14496,2,180003 +14497,3,170004 +14498,3,180004 +14499,4,140000 +14500,5,150010 +14501,5,150020 +14502,5,150030 +14503,5,150040 +14505,1,101000010 +14506,1,102000010 +14507,1,103000010 +14508,1,104000010 +14509,1,105000010 +14510,1,106000010 +14511,1,107000010 +14512,1,108000010 +14513,1,109000010 +14514,1,110000010 +14515,2,101000020 +14516,2,101000030 +14517,2,102000020 +14518,2,102000030 +14519,2,102000040 +14520,2,103000020 +14521,2,103000030 +14522,2,103000040 +14523,2,104000020 +14524,2,104000030 +14525,2,104000040 +14526,2,105000020 +14527,2,105000030 +14528,2,105000040 +14529,2,106000020 +14530,2,106000030 +14531,2,106000040 +14532,2,107000020 +14533,2,107000030 +14534,2,107000040 +14535,2,108000020 +14536,2,108000030 +14537,2,108000040 +14538,2,109000020 +14539,2,109000030 +14540,2,109000040 +14541,2,110000020 +14542,2,110000030 +14543,2,110000040 +14544,2,118000010 +14545,3,101000050 +14546,3,101000060 +14547,3,101000080 +14548,3,101000040 +14549,3,109000060 +14550,3,109000070 +14551,3,109000080 +14552,3,105000060 +14553,3,105000070 +14554,3,105000080 +14555,3,104000050 +14556,3,106000050 +14557,3,102000060 +14558,3,102000070 +14559,3,102000080 +14560,3,103000050 +14561,3,105000050 +14562,3,107000060 +14563,3,107000070 +14564,3,107000080 +14565,3,108000050 +14566,3,109000050 +14567,3,103000060 +14568,3,103000070 +14569,3,103000080 +14570,3,110000050 +14571,3,106000060 +14572,3,106000070 +14573,3,106000080 +14574,3,101000070 +14575,3,110000060 +14576,3,104000060 +14577,3,104000070 +14578,3,104000080 +14579,3,102000050 +14580,3,104000170 +14581,3,104000260 +14582,3,111000010 +14583,3,111000020 +14584,3,111000030 +14585,3,112000020 +14586,3,112000030 +14587,3,108000060 +14588,3,108000070 +14589,3,108000080 +14590,3,107000050 +14591,3,112000010 +14592,3,110000070 +14593,3,110000080 +14594,3,118000020 +14595,3,118000030 +14596,3,118000040 +14597,4,101000090 +14598,4,101000100 +14599,4,101000110 +14600,4,109000100 +14601,4,105000100 +14602,4,105000110 +14603,4,108000090 +14604,4,110000090 +14605,4,102000100 +14606,4,102000110 +14607,4,106000090 +14608,4,109000090 +14609,4,107000100 +14610,4,103000090 +14611,4,102000090 +14612,4,103000100 +14613,4,106000100 +14614,4,106000110 +14615,4,104000090 +14616,4,104000100 +14617,4,104000110 +14618,4,107000090 +14619,4,104000180 +14620,4,111000040 +14621,4,112000040 +14622,4,108000100 +14623,4,105000090 +14624,4,110000100 +14625,4,118000050 +14626,4,118000060 +14627,5,101000120 +14628,5,109000110 +14629,5,105000120 +14630,5,102000120 +14631,5,107000110 +14632,5,103000120 +14633,5,106000120 +14634,5,104000120 +14635,5,104000190 +14636,5,111000060 +14637,5,112000060 +14638,5,108000110 +14639,5,110000110 +14640,5,118000070 +14641,1,201000010 +14642,1,292000010 +14643,1,299000040 +14644,1,299000070 +14645,1,299000110 +14646,1,299000120 +14647,1,299000140 +14648,2,202000010 +14649,2,290000010 +14650,2,299000010 +14651,2,299000150 +14652,2,299000190 +14653,2,299000200 +14654,2,299000210 +14655,3,298000050 +14656,3,298000060 +14657,3,299000060 +14658,3,299000170 +14659,3,290000120 +14660,3,291000050 +14661,3,292000020 +14662,4,299000670 +14663,4,299000680 +14664,4,204000010 +14665,4,209000040 +14666,4,202000070 +14667,4,209000070 +14668,4,203000110 +14669,4,290000110 +14670,4,206000110 +14671,5,297000100 +14672,5,291000020 +14673,5,297000130 +14674,5,297000140 +14675,5,203000010 +14676,5,206000030 +14677,5,203000050 +14678,5,202000090 +14679,5,204000080 +14680,5,202000150 +14681,5,204000100 +14682,2,170003 +14683,2,180003 +14684,3,170004 +14685,3,180004 +14686,4,140000 +14687,5,150010 +14688,5,150020 +14689,5,150030 +14690,5,150040 +14692,3,101000050 +14693,3,101000060 +14694,3,101000080 +14695,3,101000040 +14696,3,109000060 +14697,3,109000070 +14698,3,109000080 +14699,3,105000060 +14700,3,105000070 +14701,3,105000080 +14702,3,104000050 +14703,3,106000050 +14704,3,102000060 +14705,3,102000070 +14706,3,102000080 +14707,3,103000050 +14708,3,105000050 +14709,3,107000060 +14710,3,107000070 +14711,3,107000080 +14712,3,108000050 +14713,3,109000050 +14714,3,103000060 +14715,3,103000070 +14716,3,103000080 +14717,3,110000050 +14718,3,106000060 +14719,3,106000070 +14720,3,106000080 +14721,3,101000070 +14722,3,110000060 +14723,3,104000060 +14724,3,104000070 +14725,3,104000080 +14726,3,102000050 +14727,3,104000170 +14728,3,104000260 +14729,3,111000010 +14730,3,111000020 +14731,3,111000030 +14732,3,112000020 +14733,3,112000030 +14734,3,108000060 +14735,3,108000070 +14736,3,108000080 +14737,3,107000050 +14738,3,112000010 +14739,3,110000070 +14740,3,110000080 +14741,3,118000020 +14742,3,118000030 +14743,3,118000040 +14744,4,101000090 +14745,4,101000100 +14746,4,101000110 +14747,4,109000100 +14748,4,105000100 +14749,4,105000110 +14750,4,108000090 +14751,4,110000090 +14752,4,102000100 +14753,4,102000110 +14754,4,106000090 +14755,4,109000090 +14756,4,107000100 +14757,4,103000090 +14758,4,102000090 +14759,4,103000100 +14760,4,106000100 +14761,4,106000110 +14762,4,104000090 +14763,4,104000100 +14764,4,104000110 +14765,4,107000090 +14766,4,104000180 +14767,4,111000040 +14768,4,112000040 +14769,4,108000100 +14770,4,105000090 +14771,4,110000100 +14772,4,118000050 +14773,4,118000060 +14774,5,101000120 +14775,5,109000110 +14776,5,105000120 +14777,5,102000120 +14778,5,107000110 +14779,5,103000120 +14780,5,106000120 +14781,5,104000120 +14782,5,104000190 +14783,5,111000060 +14784,5,112000060 +14785,5,108000110 +14786,5,110000110 +14787,5,118000070 +14788,1,201000010 +14789,1,292000010 +14790,1,299000040 +14791,1,299000070 +14792,1,299000110 +14793,1,299000120 +14794,1,299000140 +14795,2,202000010 +14796,2,290000010 +14797,2,299000010 +14798,2,299000150 +14799,2,299000190 +14800,2,299000200 +14801,2,299000210 +14802,3,298000050 +14803,3,298000060 +14804,3,299000060 +14805,3,299000170 +14806,3,290000120 +14807,3,291000050 +14808,3,292000020 +14809,4,299000670 +14810,4,299000680 +14811,4,204000010 +14812,4,209000040 +14813,4,202000070 +14814,4,209000070 +14815,4,203000110 +14816,4,290000110 +14817,4,206000110 +14818,5,297000100 +14819,5,291000020 +14820,5,297000130 +14821,5,297000140 +14822,5,203000010 +14823,5,206000030 +14824,5,203000050 +14825,5,202000090 +14826,5,204000080 +14827,5,202000150 +14828,5,204000100 +14829,3,170004 +14830,3,180004 +14831,4,140000 +14832,5,150010 +14833,5,150020 +14834,5,150030 +14835,5,150040 +14837,3,101000050 +14838,3,101000060 +14839,3,101000080 +14840,3,101000040 +14841,3,109000060 +14842,3,109000070 +14843,3,109000080 +14844,3,105000060 +14845,3,105000070 +14846,3,105000080 +14847,3,104000050 +14848,3,106000050 +14849,3,102000060 +14850,3,102000070 +14851,3,102000080 +14852,3,103000050 +14853,3,105000050 +14854,3,107000060 +14855,3,107000070 +14856,3,107000080 +14857,3,108000050 +14858,3,109000050 +14859,3,103000060 +14860,3,103000070 +14861,3,103000080 +14862,3,110000050 +14863,3,106000060 +14864,3,106000070 +14865,3,106000080 +14866,3,101000070 +14867,3,110000060 +14868,3,104000060 +14869,3,104000070 +14870,3,104000080 +14871,3,102000050 +14872,3,104000170 +14873,3,104000260 +14874,3,111000010 +14875,3,111000020 +14876,3,111000030 +14877,3,112000020 +14878,3,112000030 +14879,3,108000060 +14880,3,108000070 +14881,3,108000080 +14882,3,107000050 +14883,3,112000010 +14884,3,110000070 +14885,3,110000080 +14886,3,118000020 +14887,3,118000030 +14888,3,118000040 +14889,4,101000090 +14890,4,101000100 +14891,4,101000110 +14892,4,109000100 +14893,4,105000100 +14894,4,105000110 +14895,4,108000090 +14896,4,110000090 +14897,4,102000100 +14898,4,102000110 +14899,4,106000090 +14900,4,109000090 +14901,4,107000100 +14902,4,103000090 +14903,4,102000090 +14904,4,103000100 +14905,4,106000100 +14906,4,106000110 +14907,4,104000090 +14908,4,104000100 +14909,4,104000110 +14910,4,107000090 +14911,4,104000180 +14912,4,111000040 +14913,4,112000040 +14914,4,108000100 +14915,4,105000090 +14916,4,110000100 +14917,4,118000050 +14918,4,118000060 +14919,5,101000120 +14920,5,109000110 +14921,5,105000120 +14922,5,102000120 +14923,5,107000110 +14924,5,103000120 +14925,5,106000120 +14926,5,104000120 +14927,5,104000190 +14928,5,111000060 +14929,5,112000060 +14930,5,108000110 +14931,5,110000110 +14932,5,118000070 +14933,1,201000010 +14934,1,292000010 +14935,1,299000040 +14936,1,299000070 +14937,1,299000110 +14938,1,299000120 +14939,1,299000140 +14940,2,202000010 +14941,2,290000010 +14942,2,299000010 +14943,2,299000150 +14944,2,299000190 +14945,2,299000200 +14946,2,299000210 +14947,3,298000050 +14948,3,298000060 +14949,3,299000060 +14950,3,299000170 +14951,3,290000120 +14952,3,291000050 +14953,3,292000020 +14954,4,299000670 +14955,4,299000680 +14956,4,204000010 +14957,4,209000040 +14958,4,202000070 +14959,4,209000070 +14960,4,203000110 +14961,4,290000110 +14962,4,206000110 +14963,5,297000100 +14964,5,291000020 +14965,5,297000130 +14966,5,297000140 +14967,5,203000010 +14968,5,206000030 +14969,5,203000050 +14970,5,202000090 +14971,5,204000080 +14972,5,202000150 +14973,5,204000100 +14974,3,170004 +14975,3,180004 +14976,4,140000 +14977,5,150010 +14978,5,150020 +14979,5,150030 +14980,5,150040 +14982,3,101000050 +14983,3,101000060 +14984,3,101000080 +14985,3,101000040 +14986,3,109000060 +14987,3,109000070 +14988,3,109000080 +14989,3,105000060 +14990,3,105000070 +14991,3,105000080 +14992,3,104000050 +14993,3,106000050 +14994,3,102000060 +14995,3,102000070 +14996,3,102000080 +14997,3,103000050 +14998,3,105000050 +14999,3,107000060 +15000,3,107000070 +15001,3,107000080 +15002,3,108000050 +15003,3,109000050 +15004,3,103000060 +15005,3,103000070 +15006,3,103000080 +15007,3,110000050 +15008,3,106000060 +15009,3,106000070 +15010,3,106000080 +15011,3,101000070 +15012,3,110000060 +15013,3,104000060 +15014,3,104000070 +15015,3,104000080 +15016,3,102000050 +15017,3,104000170 +15018,3,104000260 +15019,3,111000010 +15020,3,111000020 +15021,3,111000030 +15022,3,112000020 +15023,3,112000030 +15024,3,108000060 +15025,3,108000070 +15026,3,108000080 +15027,3,107000050 +15028,3,112000010 +15029,3,110000070 +15030,3,110000080 +15031,3,118000020 +15032,3,118000030 +15033,3,118000040 +15034,4,101000090 +15035,4,101000100 +15036,4,101000110 +15037,4,109000100 +15038,4,105000100 +15039,4,105000110 +15040,4,108000090 +15041,4,110000090 +15042,4,102000100 +15043,4,102000110 +15044,4,106000090 +15045,4,109000090 +15046,4,107000100 +15047,4,103000090 +15048,4,102000090 +15049,4,103000100 +15050,4,106000100 +15051,4,106000110 +15052,4,104000090 +15053,4,104000100 +15054,4,104000110 +15055,4,107000090 +15056,4,104000180 +15057,4,111000040 +15058,4,112000040 +15059,4,108000100 +15060,4,105000090 +15061,4,110000100 +15062,4,118000050 +15063,4,118000060 +15064,5,101000120 +15065,5,109000110 +15066,5,105000120 +15067,5,102000120 +15068,5,107000110 +15069,5,103000120 +15070,5,106000120 +15071,5,104000120 +15072,5,104000190 +15073,5,111000060 +15074,5,112000060 +15075,5,108000110 +15076,5,110000110 +15077,5,118000070 +15078,1,201000010 +15079,1,292000010 +15080,1,299000040 +15081,1,299000070 +15082,1,299000110 +15083,1,299000120 +15084,1,299000140 +15085,2,202000010 +15086,2,290000010 +15087,2,299000010 +15088,2,299000150 +15089,2,299000190 +15090,2,299000200 +15091,2,299000210 +15092,3,298000050 +15093,3,298000060 +15094,3,299000060 +15095,3,299000170 +15096,3,290000120 +15097,3,291000050 +15098,3,292000020 +15099,4,299000670 +15100,4,299000680 +15101,4,204000010 +15102,4,209000040 +15103,4,202000070 +15104,4,209000070 +15105,4,203000110 +15106,4,290000110 +15107,4,206000110 +15108,5,297000100 +15109,5,291000020 +15110,5,297000130 +15111,5,297000140 +15112,5,203000010 +15113,5,206000030 +15114,5,203000050 +15115,5,202000090 +15116,5,204000080 +15117,5,202000150 +15118,5,204000100 +15119,3,170004 +15120,3,180004 +15121,4,140000 +15122,5,150010 +15123,5,150020 +15124,5,150030 +15125,5,150040 +15126,5,190000 +15127,5,200000 +15128,5,210000 +15130,3,111000010 +15131,3,111000020 +15132,3,111000030 +15133,3,112000020 +15134,3,112000030 +15135,3,108000060 +15136,3,108000070 +15137,3,108000080 +15138,3,107000050 +15139,3,112000010 +15140,3,110000070 +15141,3,110000080 +15142,4,111000040 +15143,4,112000040 +15144,4,108000100 +15145,4,105000090 +15146,4,110000100 +15147,5,111000060 +15148,5,112000060 +15149,5,108000110 +15150,5,110000110 +15151,1,108000000 +15152,2,108000001 +15153,3,108000002 +15154,4,108000003 +15155,5,108000004 +15156,1,107000000 +15157,2,107000001 +15158,3,107000002 +15159,4,107000003 +15160,5,107000004 +15161,1,120000000 +15162,2,120000001 +15163,3,120000002 +15164,4,120000003 +15165,5,120000004 +15166,3,170004 +15167,4,170005 +15168,3,180004 +15169,4,180005 +15170,4,140000 +15171,4,150010 +15172,4,150020 +15173,4,150030 +15174,4,150040 +15176,3,101000050 +15177,3,101000060 +15178,3,101000080 +15179,3,101000040 +15180,3,109000060 +15181,3,109000070 +15182,3,109000080 +15183,3,105000060 +15184,3,105000070 +15185,3,105000080 +15186,3,104000050 +15187,3,106000050 +15188,4,101000090 +15189,4,101000100 +15190,4,101000110 +15191,4,109000100 +15192,4,105000100 +15193,4,105000110 +15194,4,108000090 +15195,4,110000090 +15196,5,101000120 +15197,5,109000110 +15198,5,105000120 +15199,1,101000000 +15200,2,101000001 +15201,3,101000002 +15202,4,101000008 +15203,5,101000004 +15204,1,109000000 +15205,2,109000001 +15206,3,109000002 +15207,4,109000003 +15208,5,109000004 +15209,4,110024 +15210,4,110034 +15211,4,110044 +15212,4,110054 +15213,3,110060 +15214,3,110070 +15215,3,110080 +15216,3,110090 +15217,3,110100 +15218,3,110110 +15219,3,110120 +15220,3,110130 +15221,3,110140 +15222,3,110150 +15223,3,110160 +15224,3,110170 +15225,3,110180 +15226,3,110190 +15227,3,110200 +15228,3,110210 +15229,3,110220 +15230,3,110230 +15231,3,110240 +15232,3,110250 +15233,3,110260 +15234,3,110270 +15235,3,110620 +15236,3,110670 +15237,4,140000 +15238,4,150010 +15239,4,150020 +15240,4,150030 +15241,4,150040 +15243,3,102000060 +15244,3,102000070 +15245,3,102000080 +15246,3,103000050 +15247,3,105000050 +15248,3,107000060 +15249,3,107000070 +15250,3,107000080 +15251,3,108000050 +15252,3,109000050 +15253,3,103000060 +15254,3,103000070 +15255,3,103000080 +15256,3,110000050 +15257,4,102000100 +15258,4,102000110 +15259,4,102000350 +15260,4,106000090 +15261,4,109000090 +15262,4,107000100 +15263,4,103000090 +15264,4,102000090 +15265,4,103000100 +15266,5,102000120 +15267,5,107000110 +15268,5,103000120 +15269,1,102000000 +15270,2,102000001 +15271,3,102000002 +15272,4,102000003 +15273,5,102000004 +15274,1,105000000 +15275,2,105000001 +15276,3,105000002 +15277,4,105000003 +15278,5,105000004 +15279,1,112000000 +15280,2,112000001 +15281,3,112000002 +15282,4,112000003 +15283,5,112000004 +15284,4,120024 +15285,4,120034 +15286,4,120044 +15287,4,120054 +15288,3,120241 +15289,3,120251 +15290,3,120261 +15291,3,120271 +15292,3,120300 +15293,3,120310 +15294,3,120320 +15295,3,120330 +15296,3,120340 +15297,3,120350 +15298,3,120360 +15299,3,120370 +15300,3,120380 +15301,3,120390 +15302,3,120400 +15303,3,120410 +15304,3,120420 +15305,3,120430 +15306,3,120450 +15307,3,120460 +15308,3,120550 +15309,3,120560 +15310,3,120570 +15311,3,120990 +15312,3,121000 +15313,3,121010 +15314,3,121020 +15315,3,121100 +15316,4,140000 +15317,4,150010 +15318,4,150020 +15319,4,150030 +15320,4,150040 +15322,3,118000020 +15323,3,118000030 +15324,3,118000040 +15325,3,106000060 +15326,3,106000070 +15327,3,106000080 +15328,3,101000070 +15329,3,110000060 +15330,3,104000060 +15331,3,104000070 +15332,3,104000080 +15333,3,102000050 +15334,3,104000170 +15335,3,104000260 +15336,4,118000050 +15337,4,118000060 +15338,4,106000100 +15339,4,106000110 +15340,4,104000090 +15341,4,104000100 +15342,4,104000110 +15343,4,104000270 +15344,4,107000090 +15345,4,104000180 +15346,5,118000070 +15347,5,106000120 +15348,5,104000120 +15349,5,104000190 +15350,1,103000000 +15351,2,103000001 +15352,3,103000002 +15353,4,103000003 +15354,5,103000004 +15355,1,111000000 +15356,2,111000001 +15357,3,111000002 +15358,4,111000003 +15359,5,111000004 +15360,1,115000000 +15361,2,115000001 +15362,3,115000002 +15363,4,115000003 +15364,5,115000004 +15365,4,130024 +15366,4,130034 +15367,4,130044 +15368,4,130054 +15369,3,130060 +15370,3,130070 +15371,3,130080 +15372,3,130090 +15373,3,130100 +15374,3,130110 +15375,3,130120 +15376,3,130130 +15377,3,130140 +15378,3,130150 +15379,3,130160 +15380,3,130170 +15381,3,130180 +15382,3,130190 +15383,3,130200 +15384,3,130420 +15385,3,130510 +15386,3,130520 +15387,3,130531 +15388,3,130540 +15389,3,130660 +15390,3,130700 +15391,3,130790 +15392,3,130800 +15393,3,131130 +15394,4,140000 +15395,4,150010 +15396,4,150020 +15397,4,150030 +15398,4,150040 +15400,1,101000010 +15401,1,102000010 +15402,1,103000010 +15403,1,104000010 +15404,1,105000010 +15405,1,106000010 +15406,1,107000010 +15407,1,108000010 +15408,1,109000010 +15409,1,110000010 +15410,2,101000020 +15411,2,101000030 +15412,2,102000020 +15413,2,102000030 +15414,2,102000040 +15415,2,103000020 +15416,2,103000030 +15417,2,103000040 +15418,2,104000020 +15419,2,104000030 +15420,2,104000040 +15421,2,105000020 +15422,2,105000030 +15423,2,105000040 +15424,2,106000020 +15425,2,106000030 +15426,2,106000040 +15427,2,107000020 +15428,2,107000030 +15429,2,107000040 +15430,2,108000020 +15431,2,108000030 +15432,2,108000040 +15433,2,109000020 +15434,2,109000030 +15435,2,109000040 +15436,2,110000020 +15437,2,110000030 +15438,2,110000040 +15439,2,118000010 +15440,3,101000050 +15441,3,101000060 +15442,3,101000080 +15443,3,101000040 +15444,3,109000060 +15445,3,109000070 +15446,3,109000080 +15447,3,105000060 +15448,3,105000070 +15449,3,105000080 +15450,3,104000050 +15451,3,106000050 +15452,3,102000060 +15453,3,102000070 +15454,3,102000080 +15455,3,103000050 +15456,3,105000050 +15457,3,107000060 +15458,3,107000070 +15459,3,107000080 +15460,3,108000050 +15461,3,109000050 +15462,3,103000060 +15463,3,103000070 +15464,3,103000080 +15465,3,110000050 +15466,3,106000060 +15467,3,106000070 +15468,3,106000080 +15469,3,101000070 +15470,3,110000060 +15471,3,104000060 +15472,3,104000070 +15473,3,104000080 +15474,3,102000050 +15475,3,104000170 +15476,3,104000260 +15477,3,111000010 +15478,3,111000020 +15479,3,111000030 +15480,3,112000020 +15481,3,112000030 +15482,3,108000060 +15483,3,108000070 +15484,3,108000080 +15485,3,107000050 +15486,3,112000010 +15487,3,110000070 +15488,3,110000080 +15489,3,118000020 +15490,3,118000030 +15491,3,118000040 +15492,4,101000090 +15493,4,101000100 +15494,4,101000110 +15495,4,109000100 +15496,4,105000100 +15497,4,105000110 +15498,4,108000090 +15499,4,110000090 +15500,4,102000100 +15501,4,102000110 +15502,4,106000090 +15503,4,109000090 +15504,4,107000100 +15505,4,103000090 +15506,4,102000090 +15507,4,103000100 +15508,4,106000100 +15509,4,106000110 +15510,4,104000090 +15511,4,104000100 +15512,4,104000110 +15513,4,107000090 +15514,4,104000180 +15515,4,111000040 +15516,4,112000040 +15517,4,108000100 +15518,4,105000090 +15519,4,110000100 +15520,4,118000050 +15521,4,118000060 +15522,5,101000120 +15523,5,109000110 +15524,5,105000120 +15525,5,102000120 +15526,5,107000110 +15527,5,103000120 +15528,5,106000120 +15529,5,104000120 +15530,5,104000190 +15531,5,111000060 +15532,5,112000060 +15533,5,108000110 +15534,5,110000110 +15535,5,118000070 +15536,1,201000010 +15537,1,292000010 +15538,1,299000040 +15539,1,299000070 +15540,1,299000110 +15541,1,299000120 +15542,1,299000140 +15543,2,202000010 +15544,2,290000010 +15545,2,299000010 +15546,2,299000150 +15547,2,299000190 +15548,2,299000200 +15549,2,299000210 +15550,3,298000050 +15551,3,298000060 +15552,3,299000060 +15553,3,299000170 +15554,3,290000120 +15555,3,291000050 +15556,3,292000020 +15557,4,299000670 +15558,4,299000680 +15559,4,204000010 +15560,4,209000040 +15561,4,202000070 +15562,4,209000070 +15563,4,203000110 +15564,4,290000110 +15565,4,206000110 +15566,4,209000160 +15567,5,297000100 +15568,5,291000020 +15569,5,297000130 +15570,5,297000140 +15571,5,203000010 +15572,5,206000030 +15573,5,203000050 +15574,5,202000090 +15575,5,204000080 +15576,5,202000150 +15577,5,204000100 +15578,5,206000170 +15579,1,170002 +15580,1,180002 +15581,2,170003 +15582,2,180003 +15583,3,170004 +15584,3,180004 +15585,4,140000 +15586,5,150010 +15587,5,150020 +15588,5,150030 +15589,5,150040 +15591,1,101000010 +15592,1,102000010 +15593,1,103000010 +15594,1,104000010 +15595,1,105000010 +15596,1,106000010 +15597,1,107000010 +15598,1,108000010 +15599,1,109000010 +15600,1,110000010 +15601,2,101000020 +15602,2,101000030 +15603,2,102000020 +15604,2,102000030 +15605,2,102000040 +15606,2,103000020 +15607,2,103000030 +15608,2,103000040 +15609,2,104000020 +15610,2,104000030 +15611,2,104000040 +15612,2,105000020 +15613,2,105000030 +15614,2,105000040 +15615,2,106000020 +15616,2,106000030 +15617,2,106000040 +15618,2,107000020 +15619,2,107000030 +15620,2,107000040 +15621,2,108000020 +15622,2,108000030 +15623,2,108000040 +15624,2,109000020 +15625,2,109000030 +15626,2,109000040 +15627,2,110000020 +15628,2,110000030 +15629,2,110000040 +15630,2,118000010 +15631,3,101000050 +15632,3,101000060 +15633,3,101000080 +15634,3,101000040 +15635,3,109000060 +15636,3,109000070 +15637,3,109000080 +15638,3,105000060 +15639,3,105000070 +15640,3,105000080 +15641,3,104000050 +15642,3,106000050 +15643,3,102000060 +15644,3,102000070 +15645,3,102000080 +15646,3,103000050 +15647,3,105000050 +15648,3,107000060 +15649,3,107000070 +15650,3,107000080 +15651,3,108000050 +15652,3,109000050 +15653,3,103000060 +15654,3,103000070 +15655,3,103000080 +15656,3,110000050 +15657,3,106000060 +15658,3,106000070 +15659,3,106000080 +15660,3,101000070 +15661,3,110000060 +15662,3,104000060 +15663,3,104000070 +15664,3,104000080 +15665,3,102000050 +15666,3,104000170 +15667,3,104000260 +15668,3,111000010 +15669,3,111000020 +15670,3,111000030 +15671,3,112000020 +15672,3,112000030 +15673,3,108000060 +15674,3,108000070 +15675,3,108000080 +15676,3,107000050 +15677,3,112000010 +15678,3,110000070 +15679,3,110000080 +15680,3,118000020 +15681,3,118000030 +15682,3,118000040 +15683,4,101000090 +15684,4,101000100 +15685,4,101000110 +15686,4,109000100 +15687,4,105000100 +15688,4,105000110 +15689,4,108000090 +15690,4,110000090 +15691,4,102000100 +15692,4,102000110 +15693,4,106000090 +15694,4,109000090 +15695,4,107000100 +15696,4,103000090 +15697,4,102000090 +15698,4,103000100 +15699,4,106000100 +15700,4,106000110 +15701,4,104000090 +15702,4,104000100 +15703,4,104000110 +15704,4,107000090 +15705,4,104000180 +15706,4,111000040 +15707,4,112000040 +15708,4,108000100 +15709,4,105000090 +15710,4,110000100 +15711,4,118000050 +15712,4,118000060 +15713,5,101000120 +15714,5,109000110 +15715,5,105000120 +15716,5,102000120 +15717,5,107000110 +15718,5,103000120 +15719,5,106000120 +15720,5,104000120 +15721,5,104000190 +15722,5,111000060 +15723,5,112000060 +15724,5,108000110 +15725,5,110000110 +15726,5,118000070 +15727,1,201000010 +15728,1,292000010 +15729,1,299000040 +15730,1,299000070 +15731,1,299000110 +15732,1,299000120 +15733,1,299000140 +15734,2,202000010 +15735,2,290000010 +15736,2,299000010 +15737,2,299000150 +15738,2,299000190 +15739,2,299000200 +15740,2,299000210 +15741,3,298000050 +15742,3,298000060 +15743,3,299000060 +15744,3,299000170 +15745,3,290000120 +15746,3,291000050 +15747,3,292000020 +15748,4,299000670 +15749,4,299000680 +15750,4,204000010 +15751,4,209000040 +15752,4,202000070 +15753,4,209000070 +15754,4,203000110 +15755,4,290000110 +15756,4,206000110 +15757,4,209000160 +15758,5,297000100 +15759,5,291000020 +15760,5,297000130 +15761,5,297000140 +15762,5,203000010 +15763,5,206000030 +15764,5,203000050 +15765,5,202000090 +15766,5,204000080 +15767,5,202000150 +15768,5,204000100 +15769,5,206000170 +15770,2,170003 +15771,2,180003 +15772,3,170004 +15773,3,180004 +15774,4,140000 +15775,5,150010 +15776,5,150020 +15777,5,150030 +15778,5,150040 +15780,3,101000050 +15781,3,101000060 +15782,3,101000080 +15783,3,101000040 +15784,3,109000060 +15785,3,109000070 +15786,3,109000080 +15787,3,105000060 +15788,3,105000070 +15789,3,105000080 +15790,3,104000050 +15791,3,106000050 +15792,3,102000060 +15793,3,102000070 +15794,3,102000080 +15795,3,103000050 +15796,3,105000050 +15797,3,107000060 +15798,3,107000070 +15799,3,107000080 +15800,3,108000050 +15801,3,109000050 +15802,3,103000060 +15803,3,103000070 +15804,3,103000080 +15805,3,110000050 +15806,3,106000060 +15807,3,106000070 +15808,3,106000080 +15809,3,101000070 +15810,3,110000060 +15811,3,104000060 +15812,3,104000070 +15813,3,104000080 +15814,3,102000050 +15815,3,104000170 +15816,3,104000260 +15817,3,111000010 +15818,3,111000020 +15819,3,111000030 +15820,3,112000020 +15821,3,112000030 +15822,3,108000060 +15823,3,108000070 +15824,3,108000080 +15825,3,107000050 +15826,3,112000010 +15827,3,110000070 +15828,3,110000080 +15829,3,118000020 +15830,3,118000030 +15831,3,118000040 +15832,4,101000090 +15833,4,101000100 +15834,4,101000110 +15835,4,109000100 +15836,4,105000100 +15837,4,105000110 +15838,4,108000090 +15839,4,110000090 +15840,4,102000100 +15841,4,102000110 +15842,4,106000090 +15843,4,109000090 +15844,4,107000100 +15845,4,103000090 +15846,4,102000090 +15847,4,103000100 +15848,4,106000100 +15849,4,106000110 +15850,4,104000090 +15851,4,104000100 +15852,4,104000110 +15853,4,107000090 +15854,4,104000180 +15855,4,111000040 +15856,4,112000040 +15857,4,108000100 +15858,4,105000090 +15859,4,110000100 +15860,4,118000050 +15861,4,118000060 +15862,5,101000120 +15863,5,109000110 +15864,5,105000120 +15865,5,102000120 +15866,5,107000110 +15867,5,103000120 +15868,5,106000120 +15869,5,104000120 +15870,5,104000190 +15871,5,111000060 +15872,5,112000060 +15873,5,108000110 +15874,5,110000110 +15875,5,118000070 +15876,1,201000010 +15877,1,292000010 +15878,1,299000040 +15879,1,299000070 +15880,1,299000110 +15881,1,299000120 +15882,1,299000140 +15883,2,202000010 +15884,2,290000010 +15885,2,299000010 +15886,2,299000150 +15887,2,299000190 +15888,2,299000200 +15889,2,299000210 +15890,3,298000050 +15891,3,298000060 +15892,3,299000060 +15893,3,299000170 +15894,3,290000120 +15895,3,291000050 +15896,3,292000020 +15897,4,299000670 +15898,4,299000680 +15899,4,204000010 +15900,4,209000040 +15901,4,202000070 +15902,4,209000070 +15903,4,203000110 +15904,4,290000110 +15905,4,206000110 +15906,4,209000160 +15907,5,297000100 +15908,5,291000020 +15909,5,297000130 +15910,5,297000140 +15911,5,203000010 +15912,5,206000030 +15913,5,203000050 +15914,5,202000090 +15915,5,204000080 +15916,5,202000150 +15917,5,204000100 +15918,5,206000170 +15919,3,170004 +15920,3,180004 +15921,4,140000 +15922,5,150010 +15923,5,150020 +15924,5,150030 +15925,5,150040 +15927,3,101000050 +15928,3,101000060 +15929,3,101000080 +15930,3,101000040 +15931,3,109000060 +15932,3,109000070 +15933,3,109000080 +15934,3,105000060 +15935,3,105000070 +15936,3,105000080 +15937,3,104000050 +15938,3,106000050 +15939,3,102000060 +15940,3,102000070 +15941,3,102000080 +15942,3,103000050 +15943,3,105000050 +15944,3,107000060 +15945,3,107000070 +15946,3,107000080 +15947,3,108000050 +15948,3,109000050 +15949,3,103000060 +15950,3,103000070 +15951,3,103000080 +15952,3,110000050 +15953,3,106000060 +15954,3,106000070 +15955,3,106000080 +15956,3,101000070 +15957,3,110000060 +15958,3,104000060 +15959,3,104000070 +15960,3,104000080 +15961,3,102000050 +15962,3,104000170 +15963,3,104000260 +15964,3,111000010 +15965,3,111000020 +15966,3,111000030 +15967,3,112000020 +15968,3,112000030 +15969,3,108000060 +15970,3,108000070 +15971,3,108000080 +15972,3,107000050 +15973,3,112000010 +15974,3,110000070 +15975,3,110000080 +15976,3,118000020 +15977,3,118000030 +15978,3,118000040 +15979,4,101000090 +15980,4,101000100 +15981,4,101000110 +15982,4,109000100 +15983,4,105000100 +15984,4,105000110 +15985,4,108000090 +15986,4,110000090 +15987,4,102000100 +15988,4,102000110 +15989,4,106000090 +15990,4,109000090 +15991,4,107000100 +15992,4,103000090 +15993,4,102000090 +15994,4,103000100 +15995,4,106000100 +15996,4,106000110 +15997,4,104000090 +15998,4,104000100 +15999,4,104000110 +16000,4,107000090 +16001,4,104000180 +16002,4,111000040 +16003,4,112000040 +16004,4,108000100 +16005,4,105000090 +16006,4,110000100 +16007,4,118000050 +16008,4,118000060 +16009,5,101000120 +16010,5,109000110 +16011,5,105000120 +16012,5,102000120 +16013,5,107000110 +16014,5,103000120 +16015,5,106000120 +16016,5,104000120 +16017,5,104000190 +16018,5,111000060 +16019,5,112000060 +16020,5,108000110 +16021,5,110000110 +16022,5,118000070 +16023,1,201000010 +16024,1,292000010 +16025,1,299000040 +16026,1,299000070 +16027,1,299000110 +16028,1,299000120 +16029,1,299000140 +16030,2,202000010 +16031,2,290000010 +16032,2,299000010 +16033,2,299000150 +16034,2,299000190 +16035,2,299000200 +16036,2,299000210 +16037,3,298000050 +16038,3,298000060 +16039,3,299000060 +16040,3,299000170 +16041,3,290000120 +16042,3,291000050 +16043,3,292000020 +16044,4,299000670 +16045,4,299000680 +16046,4,204000010 +16047,4,209000040 +16048,4,202000070 +16049,4,209000070 +16050,4,203000110 +16051,4,290000110 +16052,4,206000110 +16053,4,209000160 +16054,5,297000100 +16055,5,291000020 +16056,5,297000130 +16057,5,297000140 +16058,5,203000010 +16059,5,206000030 +16060,5,203000050 +16061,5,202000090 +16062,5,204000080 +16063,5,202000150 +16064,5,204000100 +16065,5,206000170 +16066,3,170004 +16067,3,180004 +16068,4,140000 +16069,5,150010 +16070,5,150020 +16071,5,150030 +16072,5,150040 +16074,3,101000050 +16075,3,101000060 +16076,3,101000080 +16077,3,101000040 +16078,3,109000060 +16079,3,109000070 +16080,3,109000080 +16081,3,105000060 +16082,3,105000070 +16083,3,105000080 +16084,3,104000050 +16085,3,106000050 +16086,3,102000060 +16087,3,102000070 +16088,3,102000080 +16089,3,103000050 +16090,3,105000050 +16091,3,107000060 +16092,3,107000070 +16093,3,107000080 +16094,3,108000050 +16095,3,109000050 +16096,3,103000060 +16097,3,103000070 +16098,3,103000080 +16099,3,110000050 +16100,3,106000060 +16101,3,106000070 +16102,3,106000080 +16103,3,101000070 +16104,3,110000060 +16105,3,104000060 +16106,3,104000070 +16107,3,104000080 +16108,3,102000050 +16109,3,104000170 +16110,3,104000260 +16111,3,111000010 +16112,3,111000020 +16113,3,111000030 +16114,3,112000020 +16115,3,112000030 +16116,3,108000060 +16117,3,108000070 +16118,3,108000080 +16119,3,107000050 +16120,3,112000010 +16121,3,110000070 +16122,3,110000080 +16123,3,118000020 +16124,3,118000030 +16125,3,118000040 +16126,4,101000090 +16127,4,101000100 +16128,4,101000110 +16129,4,109000100 +16130,4,105000100 +16131,4,105000110 +16132,4,108000090 +16133,4,110000090 +16134,4,102000100 +16135,4,102000110 +16136,4,106000090 +16137,4,109000090 +16138,4,107000100 +16139,4,103000090 +16140,4,102000090 +16141,4,103000100 +16142,4,106000100 +16143,4,106000110 +16144,4,104000090 +16145,4,104000100 +16146,4,104000110 +16147,4,107000090 +16148,4,104000180 +16149,4,111000040 +16150,4,112000040 +16151,4,108000100 +16152,4,105000090 +16153,4,110000100 +16154,4,118000050 +16155,4,118000060 +16156,5,101000120 +16157,5,109000110 +16158,5,105000120 +16159,5,102000120 +16160,5,107000110 +16161,5,103000120 +16162,5,106000120 +16163,5,104000120 +16164,5,104000190 +16165,5,111000060 +16166,5,112000060 +16167,5,108000110 +16168,5,110000110 +16169,5,118000070 +16170,1,201000010 +16171,1,292000010 +16172,1,299000040 +16173,1,299000070 +16174,1,299000110 +16175,1,299000120 +16176,1,299000140 +16177,2,202000010 +16178,2,290000010 +16179,2,299000010 +16180,2,299000150 +16181,2,299000190 +16182,2,299000200 +16183,2,299000210 +16184,3,298000050 +16185,3,298000060 +16186,3,299000060 +16187,3,299000170 +16188,3,290000120 +16189,3,291000050 +16190,3,292000020 +16191,4,299000670 +16192,4,299000680 +16193,4,204000010 +16194,4,209000040 +16195,4,202000070 +16196,4,209000070 +16197,4,203000110 +16198,4,290000110 +16199,4,206000110 +16200,4,209000160 +16201,5,297000100 +16202,5,291000020 +16203,5,297000130 +16204,5,297000140 +16205,5,203000010 +16206,5,206000030 +16207,5,203000050 +16208,5,202000090 +16209,5,204000080 +16210,5,202000150 +16211,5,204000100 +16212,5,206000170 +16213,3,170004 +16214,3,180004 +16215,4,140000 +16216,5,150010 +16217,5,150020 +16218,5,150030 +16219,5,150040 +16220,5,190000 +16221,5,200000 +16222,5,210000 +16224,1,101000010 +16225,1,102000010 +16226,1,103000010 +16227,1,104000010 +16228,1,105000010 +16229,1,106000010 +16230,1,107000010 +16231,1,108000010 +16232,1,109000010 +16233,1,110000010 +16234,2,101000020 +16235,2,101000030 +16236,2,102000020 +16237,2,102000030 +16238,2,102000040 +16239,2,103000020 +16240,2,103000030 +16241,2,103000040 +16242,2,104000020 +16243,2,104000030 +16244,2,104000040 +16245,2,105000020 +16246,2,105000030 +16247,2,105000040 +16248,2,106000020 +16249,2,106000030 +16250,2,106000040 +16251,2,107000020 +16252,2,107000030 +16253,2,107000040 +16254,2,108000020 +16255,2,108000030 +16256,2,108000040 +16257,2,109000020 +16258,2,109000030 +16259,2,109000040 +16260,2,110000020 +16261,2,110000030 +16262,2,110000040 +16263,2,118000010 +16264,3,101000050 +16265,3,101000060 +16266,3,101000080 +16267,3,101000040 +16268,3,109000060 +16269,3,109000070 +16270,3,109000080 +16271,3,105000060 +16272,3,105000070 +16273,3,105000080 +16274,3,104000050 +16275,3,106000050 +16276,3,102000060 +16277,3,102000070 +16278,3,102000080 +16279,3,103000050 +16280,3,105000050 +16281,3,107000060 +16282,3,107000070 +16283,3,107000080 +16284,3,108000050 +16285,3,109000050 +16286,3,103000060 +16287,3,103000070 +16288,3,103000080 +16289,3,110000050 +16290,3,106000060 +16291,3,106000070 +16292,3,106000080 +16293,3,101000070 +16294,3,110000060 +16295,3,104000060 +16296,3,104000070 +16297,3,104000080 +16298,3,102000050 +16299,3,104000170 +16300,3,104000260 +16301,3,111000010 +16302,3,111000020 +16303,3,111000030 +16304,3,112000020 +16305,3,112000030 +16306,3,108000060 +16307,3,108000070 +16308,3,108000080 +16309,3,107000050 +16310,3,112000010 +16311,3,110000070 +16312,3,110000080 +16313,3,118000020 +16314,3,118000030 +16315,3,118000040 +16316,4,101000090 +16317,4,101000100 +16318,4,101000110 +16319,4,109000100 +16320,4,105000100 +16321,4,105000110 +16322,4,108000090 +16323,4,110000090 +16324,4,102000100 +16325,4,102000110 +16326,4,106000090 +16327,4,109000090 +16328,4,107000100 +16329,4,103000090 +16330,4,102000090 +16331,4,103000100 +16332,4,106000100 +16333,4,106000110 +16334,4,104000090 +16335,4,104000100 +16336,4,104000110 +16337,4,107000090 +16338,4,104000180 +16339,4,111000040 +16340,4,112000040 +16341,4,108000100 +16342,4,105000090 +16343,4,110000100 +16344,4,118000050 +16345,4,118000060 +16346,5,101000120 +16347,5,109000110 +16348,5,105000120 +16349,5,102000120 +16350,5,107000110 +16351,5,103000120 +16352,5,106000120 +16353,5,104000120 +16354,5,104000190 +16355,5,111000060 +16356,5,112000060 +16357,5,108000110 +16358,5,110000110 +16359,5,118000070 +16360,1,201000010 +16361,1,292000010 +16362,1,299000040 +16363,1,299000070 +16364,1,299000110 +16365,1,299000120 +16366,1,299000140 +16367,2,202000010 +16368,2,290000010 +16369,2,299000010 +16370,2,299000150 +16371,2,299000190 +16372,2,299000200 +16373,2,299000210 +16374,3,298000050 +16375,3,298000060 +16376,3,299000060 +16377,3,299000170 +16378,3,290000120 +16379,3,291000050 +16380,3,292000020 +16381,4,299000670 +16382,4,299000680 +16383,4,204000010 +16384,4,209000040 +16385,4,202000070 +16386,4,209000070 +16387,4,203000110 +16388,4,290000110 +16389,4,206000110 +16390,4,209000160 +16391,4,209000190 +16392,5,297000100 +16393,5,291000020 +16394,5,297000130 +16395,5,297000140 +16396,5,203000010 +16397,5,206000030 +16398,5,203000050 +16399,5,202000090 +16400,5,204000080 +16401,5,202000150 +16402,5,204000100 +16403,5,206000170 +16404,5,204000200 +16405,1,170002 +16406,1,180002 +16407,2,170003 +16408,2,180003 +16409,3,170004 +16410,3,180004 +16411,4,140000 +16412,5,150010 +16413,5,150020 +16414,5,150030 +16415,5,150040 +16417,1,101000010 +16418,1,102000010 +16419,1,103000010 +16420,1,104000010 +16421,1,105000010 +16422,1,106000010 +16423,1,107000010 +16424,1,108000010 +16425,1,109000010 +16426,1,110000010 +16427,2,101000020 +16428,2,101000030 +16429,2,102000020 +16430,2,102000030 +16431,2,102000040 +16432,2,103000020 +16433,2,103000030 +16434,2,103000040 +16435,2,104000020 +16436,2,104000030 +16437,2,104000040 +16438,2,105000020 +16439,2,105000030 +16440,2,105000040 +16441,2,106000020 +16442,2,106000030 +16443,2,106000040 +16444,2,107000020 +16445,2,107000030 +16446,2,107000040 +16447,2,108000020 +16448,2,108000030 +16449,2,108000040 +16450,2,109000020 +16451,2,109000030 +16452,2,109000040 +16453,2,110000020 +16454,2,110000030 +16455,2,110000040 +16456,2,118000010 +16457,3,101000050 +16458,3,101000060 +16459,3,101000080 +16460,3,101000040 +16461,3,109000060 +16462,3,109000070 +16463,3,109000080 +16464,3,105000060 +16465,3,105000070 +16466,3,105000080 +16467,3,104000050 +16468,3,106000050 +16469,3,102000060 +16470,3,102000070 +16471,3,102000080 +16472,3,103000050 +16473,3,105000050 +16474,3,107000060 +16475,3,107000070 +16476,3,107000080 +16477,3,108000050 +16478,3,109000050 +16479,3,103000060 +16480,3,103000070 +16481,3,103000080 +16482,3,110000050 +16483,3,106000060 +16484,3,106000070 +16485,3,106000080 +16486,3,101000070 +16487,3,110000060 +16488,3,104000060 +16489,3,104000070 +16490,3,104000080 +16491,3,102000050 +16492,3,104000170 +16493,3,104000260 +16494,3,111000010 +16495,3,111000020 +16496,3,111000030 +16497,3,112000020 +16498,3,112000030 +16499,3,108000060 +16500,3,108000070 +16501,3,108000080 +16502,3,107000050 +16503,3,112000010 +16504,3,110000070 +16505,3,110000080 +16506,3,118000020 +16507,3,118000030 +16508,3,118000040 +16509,4,101000090 +16510,4,101000100 +16511,4,101000110 +16512,4,109000100 +16513,4,105000100 +16514,4,105000110 +16515,4,108000090 +16516,4,110000090 +16517,4,102000100 +16518,4,102000110 +16519,4,106000090 +16520,4,109000090 +16521,4,107000100 +16522,4,103000090 +16523,4,102000090 +16524,4,103000100 +16525,4,106000100 +16526,4,106000110 +16527,4,104000090 +16528,4,104000100 +16529,4,104000110 +16530,4,107000090 +16531,4,104000180 +16532,4,111000040 +16533,4,112000040 +16534,4,108000100 +16535,4,105000090 +16536,4,110000100 +16537,4,118000050 +16538,4,118000060 +16539,5,101000120 +16540,5,109000110 +16541,5,105000120 +16542,5,102000120 +16543,5,107000110 +16544,5,103000120 +16545,5,106000120 +16546,5,104000120 +16547,5,104000190 +16548,5,111000060 +16549,5,112000060 +16550,5,108000110 +16551,5,110000110 +16552,5,118000070 +16553,1,201000010 +16554,1,292000010 +16555,1,299000040 +16556,1,299000070 +16557,1,299000110 +16558,1,299000120 +16559,1,299000140 +16560,2,202000010 +16561,2,290000010 +16562,2,299000010 +16563,2,299000150 +16564,2,299000190 +16565,2,299000200 +16566,2,299000210 +16567,3,298000050 +16568,3,298000060 +16569,3,299000060 +16570,3,299000170 +16571,3,290000120 +16572,3,291000050 +16573,3,292000020 +16574,4,299000670 +16575,4,299000680 +16576,4,204000010 +16577,4,209000040 +16578,4,202000070 +16579,4,209000070 +16580,4,203000110 +16581,4,290000110 +16582,4,206000110 +16583,4,209000160 +16584,4,209000190 +16585,5,297000100 +16586,5,291000020 +16587,5,297000130 +16588,5,297000140 +16589,5,203000010 +16590,5,206000030 +16591,5,203000050 +16592,5,202000090 +16593,5,204000080 +16594,5,202000150 +16595,5,204000100 +16596,5,206000170 +16597,5,204000200 +16598,2,170003 +16599,2,180003 +16600,3,170004 +16601,3,180004 +16602,4,140000 +16603,5,150010 +16604,5,150020 +16605,5,150030 +16606,5,150040 +16608,3,101000050 +16609,3,101000060 +16610,3,101000080 +16611,3,101000040 +16612,3,109000060 +16613,3,109000070 +16614,3,109000080 +16615,3,105000060 +16616,3,105000070 +16617,3,105000080 +16618,3,104000050 +16619,3,106000050 +16620,3,102000060 +16621,3,102000070 +16622,3,102000080 +16623,3,103000050 +16624,3,105000050 +16625,3,107000060 +16626,3,107000070 +16627,3,107000080 +16628,3,108000050 +16629,3,109000050 +16630,3,103000060 +16631,3,103000070 +16632,3,103000080 +16633,3,110000050 +16634,3,106000060 +16635,3,106000070 +16636,3,106000080 +16637,3,101000070 +16638,3,110000060 +16639,3,104000060 +16640,3,104000070 +16641,3,104000080 +16642,3,102000050 +16643,3,104000170 +16644,3,104000260 +16645,3,111000010 +16646,3,111000020 +16647,3,111000030 +16648,3,112000020 +16649,3,112000030 +16650,3,108000060 +16651,3,108000070 +16652,3,108000080 +16653,3,107000050 +16654,3,112000010 +16655,3,110000070 +16656,3,110000080 +16657,3,118000020 +16658,3,118000030 +16659,3,118000040 +16660,4,101000090 +16661,4,101000100 +16662,4,101000110 +16663,4,109000100 +16664,4,105000100 +16665,4,105000110 +16666,4,108000090 +16667,4,110000090 +16668,4,102000100 +16669,4,102000110 +16670,4,106000090 +16671,4,109000090 +16672,4,107000100 +16673,4,103000090 +16674,4,102000090 +16675,4,103000100 +16676,4,106000100 +16677,4,106000110 +16678,4,104000090 +16679,4,104000100 +16680,4,104000110 +16681,4,107000090 +16682,4,104000180 +16683,4,111000040 +16684,4,112000040 +16685,4,108000100 +16686,4,105000090 +16687,4,110000100 +16688,4,118000050 +16689,4,118000060 +16690,5,101000120 +16691,5,109000110 +16692,5,105000120 +16693,5,102000120 +16694,5,107000110 +16695,5,103000120 +16696,5,106000120 +16697,5,104000120 +16698,5,104000190 +16699,5,111000060 +16700,5,112000060 +16701,5,108000110 +16702,5,110000110 +16703,5,118000070 +16704,1,201000010 +16705,1,292000010 +16706,1,299000040 +16707,1,299000070 +16708,1,299000110 +16709,1,299000120 +16710,1,299000140 +16711,2,202000010 +16712,2,290000010 +16713,2,299000010 +16714,2,299000150 +16715,2,299000190 +16716,2,299000200 +16717,2,299000210 +16718,3,298000050 +16719,3,298000060 +16720,3,299000060 +16721,3,299000170 +16722,3,290000120 +16723,3,291000050 +16724,3,292000020 +16725,4,299000670 +16726,4,299000680 +16727,4,204000010 +16728,4,209000040 +16729,4,202000070 +16730,4,209000070 +16731,4,203000110 +16732,4,290000110 +16733,4,206000110 +16734,4,209000160 +16735,4,209000190 +16736,5,297000100 +16737,5,291000020 +16738,5,297000130 +16739,5,297000140 +16740,5,203000010 +16741,5,206000030 +16742,5,203000050 +16743,5,202000090 +16744,5,204000080 +16745,5,202000150 +16746,5,204000100 +16747,5,206000170 +16748,5,204000200 +16749,3,170004 +16750,3,180004 +16751,4,140000 +16752,5,150010 +16753,5,150020 +16754,5,150030 +16755,5,150040 +16757,3,101000050 +16758,3,101000060 +16759,3,101000080 +16760,3,101000040 +16761,3,109000060 +16762,3,109000070 +16763,3,109000080 +16764,3,105000060 +16765,3,105000070 +16766,3,105000080 +16767,3,104000050 +16768,3,106000050 +16769,3,102000060 +16770,3,102000070 +16771,3,102000080 +16772,3,103000050 +16773,3,105000050 +16774,3,107000060 +16775,3,107000070 +16776,3,107000080 +16777,3,108000050 +16778,3,109000050 +16779,3,103000060 +16780,3,103000070 +16781,3,103000080 +16782,3,110000050 +16783,3,106000060 +16784,3,106000070 +16785,3,106000080 +16786,3,101000070 +16787,3,110000060 +16788,3,104000060 +16789,3,104000070 +16790,3,104000080 +16791,3,102000050 +16792,3,104000170 +16793,3,104000260 +16794,3,111000010 +16795,3,111000020 +16796,3,111000030 +16797,3,112000020 +16798,3,112000030 +16799,3,108000060 +16800,3,108000070 +16801,3,108000080 +16802,3,107000050 +16803,3,112000010 +16804,3,110000070 +16805,3,110000080 +16806,3,118000020 +16807,3,118000030 +16808,3,118000040 +16809,4,101000090 +16810,4,101000100 +16811,4,101000110 +16812,4,109000100 +16813,4,105000100 +16814,4,105000110 +16815,4,108000090 +16816,4,110000090 +16817,4,102000100 +16818,4,102000110 +16819,4,106000090 +16820,4,109000090 +16821,4,107000100 +16822,4,103000090 +16823,4,102000090 +16824,4,103000100 +16825,4,106000100 +16826,4,106000110 +16827,4,104000090 +16828,4,104000100 +16829,4,104000110 +16830,4,107000090 +16831,4,104000180 +16832,4,111000040 +16833,4,112000040 +16834,4,108000100 +16835,4,105000090 +16836,4,110000100 +16837,4,118000050 +16838,4,118000060 +16839,5,101000120 +16840,5,109000110 +16841,5,105000120 +16842,5,102000120 +16843,5,107000110 +16844,5,103000120 +16845,5,106000120 +16846,5,104000120 +16847,5,104000190 +16848,5,111000060 +16849,5,112000060 +16850,5,108000110 +16851,5,110000110 +16852,5,118000070 +16853,1,201000010 +16854,1,292000010 +16855,1,299000040 +16856,1,299000070 +16857,1,299000110 +16858,1,299000120 +16859,1,299000140 +16860,2,202000010 +16861,2,290000010 +16862,2,299000010 +16863,2,299000150 +16864,2,299000190 +16865,2,299000200 +16866,2,299000210 +16867,3,298000050 +16868,3,298000060 +16869,3,299000060 +16870,3,299000170 +16871,3,290000120 +16872,3,291000050 +16873,3,292000020 +16874,4,299000670 +16875,4,299000680 +16876,4,204000010 +16877,4,209000040 +16878,4,202000070 +16879,4,209000070 +16880,4,203000110 +16881,4,290000110 +16882,4,206000110 +16883,4,209000160 +16884,4,209000190 +16885,5,297000100 +16886,5,291000020 +16887,5,297000130 +16888,5,297000140 +16889,5,203000010 +16890,5,206000030 +16891,5,203000050 +16892,5,202000090 +16893,5,204000080 +16894,5,202000150 +16895,5,204000100 +16896,5,206000170 +16897,5,204000200 +16898,3,170004 +16899,3,180004 +16900,4,140000 +16901,5,150010 +16902,5,150020 +16903,5,150030 +16904,5,150040 +16906,3,101000050 +16907,3,101000060 +16908,3,101000080 +16909,3,101000040 +16910,3,109000060 +16911,3,109000070 +16912,3,109000080 +16913,3,105000060 +16914,3,105000070 +16915,3,105000080 +16916,3,104000050 +16917,3,106000050 +16918,3,102000060 +16919,3,102000070 +16920,3,102000080 +16921,3,103000050 +16922,3,105000050 +16923,3,107000060 +16924,3,107000070 +16925,3,107000080 +16926,3,108000050 +16927,3,109000050 +16928,3,103000060 +16929,3,103000070 +16930,3,103000080 +16931,3,110000050 +16932,3,106000060 +16933,3,106000070 +16934,3,106000080 +16935,3,101000070 +16936,3,110000060 +16937,3,104000060 +16938,3,104000070 +16939,3,104000080 +16940,3,102000050 +16941,3,104000170 +16942,3,104000260 +16943,3,111000010 +16944,3,111000020 +16945,3,111000030 +16946,3,112000020 +16947,3,112000030 +16948,3,108000060 +16949,3,108000070 +16950,3,108000080 +16951,3,107000050 +16952,3,112000010 +16953,3,110000070 +16954,3,110000080 +16955,3,118000020 +16956,3,118000030 +16957,3,118000040 +16958,4,101000090 +16959,4,101000100 +16960,4,101000110 +16961,4,109000100 +16962,4,105000100 +16963,4,105000110 +16964,4,108000090 +16965,4,110000090 +16966,4,102000100 +16967,4,102000110 +16968,4,106000090 +16969,4,109000090 +16970,4,107000100 +16971,4,103000090 +16972,4,102000090 +16973,4,103000100 +16974,4,106000100 +16975,4,106000110 +16976,4,104000090 +16977,4,104000100 +16978,4,104000110 +16979,4,107000090 +16980,4,104000180 +16981,4,111000040 +16982,4,112000040 +16983,4,108000100 +16984,4,105000090 +16985,4,110000100 +16986,4,118000050 +16987,4,118000060 +16988,5,101000120 +16989,5,109000110 +16990,5,105000120 +16991,5,102000120 +16992,5,107000110 +16993,5,103000120 +16994,5,106000120 +16995,5,104000120 +16996,5,104000190 +16997,5,111000060 +16998,5,112000060 +16999,5,108000110 +17000,5,110000110 +17001,5,118000070 +17002,1,201000010 +17003,1,292000010 +17004,1,299000040 +17005,1,299000070 +17006,1,299000110 +17007,1,299000120 +17008,1,299000140 +17009,2,202000010 +17010,2,290000010 +17011,2,299000010 +17012,2,299000150 +17013,2,299000190 +17014,2,299000200 +17015,2,299000210 +17016,3,298000050 +17017,3,298000060 +17018,3,299000060 +17019,3,299000170 +17020,3,290000120 +17021,3,291000050 +17022,3,292000020 +17023,4,299000670 +17024,4,299000680 +17025,4,204000010 +17026,4,209000040 +17027,4,202000070 +17028,4,209000070 +17029,4,203000110 +17030,4,290000110 +17031,4,206000110 +17032,4,209000160 +17033,4,209000190 +17034,5,297000100 +17035,5,291000020 +17036,5,297000130 +17037,5,297000140 +17038,5,203000010 +17039,5,206000030 +17040,5,203000050 +17041,5,202000090 +17042,5,204000080 +17043,5,202000150 +17044,5,204000100 +17045,5,206000170 +17046,5,204000200 +17047,3,170004 +17048,3,180004 +17049,4,140000 +17050,5,150010 +17051,5,150020 +17052,5,150030 +17053,5,150040 +17054,5,190000 +17055,5,200000 +17057,3,118000020 +17058,3,118000030 +17059,3,118000040 +17060,3,106000060 +17061,3,106000070 +17062,3,106000080 +17063,3,101000070 +17064,3,110000060 +17065,3,104000060 +17066,3,104000070 +17067,3,104000080 +17068,3,102000050 +17069,3,104000170 +17070,3,104000260 +17071,4,118000050 +17072,4,118000060 +17073,4,106000100 +17074,4,106000110 +17075,4,104000090 +17076,4,104000100 +17077,4,104000110 +17078,4,104000270 +17079,4,107000090 +17080,4,104000180 +17081,5,118000070 +17082,5,106000120 +17083,5,104000120 +17084,5,104000190 +17085,1,103000000 +17086,2,103000001 +17087,3,103000002 +17088,4,103000003 +17089,5,103000004 +17090,1,111000000 +17091,2,111000001 +17092,3,111000002 +17093,4,111000003 +17094,5,111000004 +17095,1,115000000 +17096,2,115000001 +17097,3,115000002 +17098,4,115000003 +17099,5,115000004 +17100,3,170004 +17101,4,170005 +17102,3,180004 +17103,4,180005 +17104,4,140000 +17105,4,150010 +17106,4,150020 +17107,4,150030 +17108,4,150040 +17110,3,111000010 +17111,3,111000020 +17112,3,111000030 +17113,3,112000020 +17114,3,112000030 +17115,3,108000060 +17116,3,108000070 +17117,3,108000080 +17118,3,107000050 +17119,3,112000010 +17120,3,110000070 +17121,3,110000080 +17122,4,111000040 +17123,4,112000040 +17124,4,108000100 +17125,4,105000090 +17126,4,110000100 +17127,5,111000060 +17128,5,112000060 +17129,5,108000110 +17130,5,110000110 +17131,1,108000000 +17132,2,108000001 +17133,3,108000002 +17134,4,108000003 +17135,5,108000004 +17136,1,107000000 +17137,2,107000001 +17138,3,107000002 +17139,4,107000003 +17140,5,107000004 +17141,1,120000000 +17142,2,120000001 +17143,3,120000002 +17144,4,120000003 +17145,5,120000004 +17146,4,110024 +17147,4,110034 +17148,4,110044 +17149,4,110054 +17150,3,110060 +17151,3,110070 +17152,3,110080 +17153,3,110090 +17154,3,110100 +17155,3,110110 +17156,3,110120 +17157,3,110130 +17158,3,110140 +17159,3,110150 +17160,3,110160 +17161,3,110170 +17162,3,110180 +17163,3,110190 +17164,3,110200 +17165,3,110210 +17166,3,110220 +17167,3,110230 +17168,3,110240 +17169,3,110250 +17170,3,110260 +17171,3,110270 +17172,3,110620 +17173,3,110670 +17174,4,140000 +17175,4,150010 +17176,4,150020 +17177,4,150030 +17178,4,150040 +17180,3,101000050 +17181,3,101000060 +17182,3,101000080 +17183,3,101000040 +17184,3,109000060 +17185,3,109000070 +17186,3,109000080 +17187,3,105000060 +17188,3,105000070 +17189,3,105000080 +17190,3,104000050 +17191,3,106000050 +17192,4,101000090 +17193,4,101000100 +17194,4,101000110 +17195,4,109000100 +17196,4,105000100 +17197,4,105000110 +17198,4,108000090 +17199,4,110000090 +17200,5,101000120 +17201,5,109000110 +17202,5,105000120 +17203,1,101000000 +17204,2,101000001 +17205,3,101000002 +17206,4,101000008 +17207,5,101000004 +17208,1,109000000 +17209,2,109000001 +17210,3,109000002 +17211,4,109000003 +17212,5,109000004 +17213,4,120024 +17214,4,120034 +17215,4,120044 +17216,4,120054 +17217,3,120241 +17218,3,120251 +17219,3,120261 +17220,3,120271 +17221,3,120300 +17222,3,120310 +17223,3,120320 +17224,3,120330 +17225,3,120340 +17226,3,120350 +17227,3,120360 +17228,3,120370 +17229,3,120380 +17230,3,120390 +17231,3,120400 +17232,3,120410 +17233,3,120420 +17234,3,120430 +17235,3,120450 +17236,3,120460 +17237,3,120550 +17238,3,120560 +17239,3,120570 +17240,3,120990 +17241,3,121000 +17242,3,121010 +17243,3,121020 +17244,3,121100 +17245,4,140000 +17246,4,150010 +17247,4,150020 +17248,4,150030 +17249,4,150040 +17251,3,102000060 +17252,3,102000070 +17253,3,102000080 +17254,3,103000050 +17255,3,105000050 +17256,3,107000060 +17257,3,107000070 +17258,3,107000080 +17259,3,108000050 +17260,3,109000050 +17261,3,103000060 +17262,3,103000070 +17263,3,103000080 +17264,3,110000050 +17265,4,102000100 +17266,4,102000110 +17267,4,102000350 +17268,4,106000090 +17269,4,109000090 +17270,4,107000100 +17271,4,103000090 +17272,4,102000090 +17273,4,103000100 +17274,5,102000120 +17275,5,107000110 +17276,5,103000120 +17277,1,102000000 +17278,2,102000001 +17279,3,102000002 +17280,4,102000003 +17281,5,102000004 +17282,1,105000000 +17283,2,105000001 +17284,3,105000002 +17285,4,105000003 +17286,5,105000004 +17287,1,112000000 +17288,2,112000001 +17289,3,112000002 +17290,4,112000003 +17291,5,112000004 +17292,4,130024 +17293,4,130034 +17294,4,130044 +17295,4,130054 +17296,3,130060 +17297,3,130070 +17298,3,130080 +17299,3,130090 +17300,3,130100 +17301,3,130110 +17302,3,130120 +17303,3,130130 +17304,3,130140 +17305,3,130150 +17306,3,130160 +17307,3,130170 +17308,3,130180 +17309,3,130190 +17310,3,130200 +17311,3,130420 +17312,3,130510 +17313,3,130520 +17314,3,130531 +17315,3,130540 +17316,3,130660 +17317,3,130700 +17318,3,130790 +17319,3,130800 +17320,3,131130 +17321,4,140000 +17322,4,150010 +17323,4,150020 +17324,4,150030 +17325,4,150040 +17327,4,101000250 +17328,4,102000260 +17329,4,103000220 +17330,4,104000250 +17331,4,105000210 +17332,4,106000210 +17333,4,107000140 +17334,4,108000150 +17335,4,109000230 +17336,4,110000170 +17337,4,111000140 +17338,4,112000110 +17339,4,118000140 +17340,5,101000300 +17341,5,102000360 +17342,5,103000310 +17343,5,104000370 +17344,5,109000290 +17345,5,101000310 +17346,5,102000410 +17347,1,201000010 +17348,1,292000010 +17349,1,299000040 +17350,1,299000070 +17351,1,299000110 +17352,1,299000120 +17353,1,299000140 +17354,2,202000010 +17355,2,290000010 +17356,2,299000010 +17357,2,299000150 +17358,2,299000190 +17359,2,299000200 +17360,2,299000210 +17361,3,290000120 +17362,3,291000050 +17363,3,292000020 +17364,3,298000050 +17365,3,298000060 +17366,3,299000060 +17367,3,299000170 +17368,4,201000170 +17369,4,202000070 +17370,4,202000370 +17371,4,202000400 +17372,4,202000440 +17373,4,203000110 +17374,4,203000270 +17375,4,203000350 +17376,4,204000010 +17377,4,204000370 +17378,4,205000220 +17379,4,206000110 +17380,4,206000240 +17381,4,206000270 +17382,4,209000040 +17383,4,209000070 +17384,4,209000160 +17385,4,209000190 +17386,4,209000240 +17387,4,215000150 +17388,4,290000110 +17389,4,290000130 +17390,4,298000120 +17391,4,299000670 +17392,4,299000680 +17393,4,201000090 +17394,4,202000160 +17395,4,203000150 +17396,4,204000110 +17397,4,205000110 +17398,4,206000120 +17399,4,211000060 +17400,4,212000050 +17401,4,299000440 +17402,4,299000450 +17403,4,299000460 +17404,4,299000470 +17405,4,299000480 +17406,4,299000550 +17407,4,299000560 +17408,4,299000570 +17409,4,299000580 +17410,4,299000590 +17411,4,299000600 +17412,4,299000610 +17413,4,299000620 +17414,4,299000630 +17415,4,299000640 +17416,4,299000650 +17417,4,299000230 +17418,4,299000240 +17419,4,299000250 +17420,4,299000260 +17421,4,299000270 +17422,4,299000280 +17423,4,299000290 +17424,4,299000300 +17425,4,299000310 +17426,4,299000320 +17427,4,299000330 +17428,4,299000340 +17429,4,299000350 +17430,4,299000360 +17431,4,299000370 +17432,4,299000380 +17433,4,299000390 +17434,4,299000400 +17435,4,215000050 +17436,4,216000060 +17437,4,217000020 +17438,4,218000030 +17439,4,299000500 +17440,4,299000510 +17441,4,299000520 +17442,4,299000530 +17443,4,299000540 +17444,4,299000660 +17445,5,202000090 +17446,5,202000150 +17447,5,203000010 +17448,5,203000050 +17449,5,203000260 +17450,5,204000080 +17451,5,204000100 +17452,5,204000200 +17453,5,204000270 +17454,5,206000030 +17455,5,206000170 +17456,5,209000320 +17457,5,220000080 +17458,5,290000160 +17459,5,291000020 +17460,5,297000100 +17461,5,297000130 +17462,5,297000140 +17463,5,298000110 +17464,5,202000290 +17465,5,203000240 +17466,5,204000210 +17467,5,205000150 +17468,5,206000200 +17469,5,211000090 +17470,5,212000060 +17471,5,299000800 +17472,5,299000810 +17473,5,299000820 +17474,5,299000840 +17475,5,299000850 +17476,5,299000860 +17477,5,299000870 +17478,5,299000880 +17479,5,299000890 +17480,5,201000120 +17481,5,202000240 +17482,5,211000080 +17483,5,299000730 +17484,5,299000740 +17485,5,299000750 +17486,5,299000760 +17487,5,299000770 +17488,5,201000130 +17489,5,299000830 +17490,5,202000280 +17491,5,204000220 +17492,5,201000150 +17493,5,202000350 +17494,5,205000200 +17495,5,299001050 +17496,5,299001060 +17497,5,201000200 +17498,5,202000430 +17499,5,211000150 +17500,5,299001170 +17501,5,299001180 +17502,5,202000270 +17503,5,206000190 +17504,5,220000060 +17505,5,201000140 +17506,5,203000230 +17507,5,205000160 +17508,5,204000230 +17509,5,209000200 +17510,5,299000900 +17511,5,202000340 +17512,5,203000280 +17513,5,205000190 +17514,5,208000040 +17515,5,211000110 +17516,5,220000070 +17517,5,202000420 +17518,5,203000340 +17519,5,204000360 +17520,5,211000140 +17521,5,212000090 +17522,5,299000920 +17523,5,299000930 +17524,5,299000940 +17525,5,299000950 +17526,5,299000960 +17527,5,298000130 +17528,5,202000380 +17529,5,203000300 +17530,5,204000320 +17531,5,205000230 +17532,5,206000250 +17533,5,209000280 +17534,5,210000030 +17535,5,211000120 +17536,5,212000070 +17537,5,215000130 +17538,5,216000130 +17539,5,217000080 +17540,5,218000090 +17541,5,299001070 +17542,5,299001080 +17543,5,299001090 +17544,5,215000120 +17545,5,202000390 +17546,5,203000310 +17547,5,204000330 +17548,5,205000240 +17549,5,206000260 +17550,5,209000290 +17551,5,210000040 +17552,5,211000130 +17553,5,212000080 +17554,5,215000140 +17555,5,216000140 +17556,5,217000090 +17557,5,218000100 +17558,5,220000090 +17559,5,299001100 +17560,5,299001110 +17561,5,299001120 +17562,5,299001130 +17563,5,299001140 +17564,5,299001150 +17565,5,299000430 +17566,5,299000690 +17567,5,299000710 +17568,5,299000720 +17569,5,215000100 +17570,5,216000090 +17571,5,217000070 +17572,5,218000080 +17573,5,299000970 +17574,5,299000980 +17575,5,299000990 +17576,5,299001000 +17577,5,299001010 +17578,5,299001020 +17579,5,299001030 +17580,5,299001040 +17581,3,101000006 +17582,3,103000005 +17583,4,101000003 +17584,4,102000005 +17585,4,107000005 +17586,4,112000006 +17587,5,101000009 +17588,5,101000011 +17589,5,101000012 +17590,5,101000013 +17591,5,101000014 +17592,5,101000015 +17593,5,101000016 +17594,5,101000017 +17595,5,101000018 +17596,5,102000008 +17597,5,102000009 +17598,5,107000007 +17599,5,109000008 +17600,5,111000007 +17601,5,111000008 +17602,5,112000008 +17603,5,120000008 +17605,2,118000010 +17606,3,118000020 +17607,3,118000030 +17608,3,118000040 +17609,4,118000050 +17610,4,118000060 +17611,5,118000070 +17612,1,101000000 +17613,3,101000006 +17614,3,103000005 +17615,4,101000003 +17616,4,102000005 +17617,4,107000005 +17618,4,112000006 +17619,5,101000009 +17620,5,101000011 +17621,5,101000012 +17622,5,101000013 +17623,5,101000014 +17624,5,101000015 +17625,5,101000016 +17626,5,101000017 +17627,5,101000018 +17628,5,102000008 +17629,5,102000009 +17630,5,107000007 +17631,5,109000008 +17632,5,111000007 +17633,5,111000008 +17634,5,112000008 +17635,5,120000008 +17636,3,110540 +17637,3,110680 +17638,3,110790 +17639,3,110800 +17640,3,120440 +17641,5,110055 +17642,5,110241 +17643,5,110251 +17644,5,110261 +17645,5,110271 +17646,5,110730 +17647,5,111020 +17648,5,111100 +17649,5,111160 +17650,5,120551 +17651,5,121160 diff --git a/titles/sao/index.py b/titles/sao/index.py index 2c903ce..b74e500 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -113,5 +113,4 @@ class SaoServlet(resource.Resource): self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") - self.logger.debug(f"Response: {handler(sao_request).hex()}") return handler(sao_request) \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index 9e96aaf..ca000b8 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -246,6 +246,14 @@ class SaoStaticData(BaseData): return None return [list[2] for list in result.fetchall()] + def get_hero_id(self, heroLogId: int) -> Optional[Dict]: + sql = hero.select(hero.c.heroLogId == heroLogId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: sql = hero.select(hero.c.version == version and hero.c.enabled == enabled).order_by( hero.c.heroLogId.asc() From bf6d126f8a8809bcda127f5850a53a7b8f9a0829 Mon Sep 17 00:00:00 2001 From: Midorica Date: Tue, 30 May 2023 18:03:52 -0400 Subject: [PATCH 237/589] Equipments saving for SAO now completed --- readme.md | 3 + titles/sao/base.py | 26 +++++-- titles/sao/data/EquipmentLevel.csv | 121 +++++++++++++++++++++++++++++ titles/sao/handlers/base.py | 68 ++++++++++++---- titles/sao/schema/item.py | 67 +++++++++++++++- titles/sao/schema/static.py | 8 ++ 6 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 titles/sao/data/EquipmentLevel.csv diff --git a/readme.md b/readme.md index ee407cd..f16010e 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,9 @@ Games listed below have been tested and confirmed working. Only game versions ol + POKKÉN TOURNAMENT + Final Online ++ Sword Art Online Arcade (partial support) + + Final + ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) - pip diff --git a/titles/sao/base.py b/titles/sao/base.py index 62fa367..543f488 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -81,6 +81,9 @@ class SaoBase: self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) + self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) self.logger.info(f"User Authenticated: { access_code } | { user_id }") @@ -145,9 +148,19 @@ class SaoBase: def handle_c602(self, request: Any) -> bytes: #have_object/get_equipment_user_data_list - equipmentIdsData = self.game_data.static.get_equipment_ids(0, True) - - resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipmentIdsData) + req = bytes.fromhex(request)[24:] + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + equipment_data = self.game_data.item.get_user_equipments(user_id) + + resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipment_data) return resp.make() def handle_c604(self, request: Any) -> bytes: @@ -510,10 +523,11 @@ class SaoBase: randomized_unanalyzed_id = choice(data_unanalyzed) heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) if heroList: self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) - - # Item and Equipments saving will be done later here + if equipmentList: + self.game_data.item.put_equipment_data(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) # Send response @@ -532,4 +546,4 @@ class SaoBase: def handle_c90a(self, request: Any) -> bytes: #should be tweaked for proper item unlock #quest/episode_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) - return resp.make() \ No newline at end of file + return resp.make() diff --git a/titles/sao/data/EquipmentLevel.csv b/titles/sao/data/EquipmentLevel.csv new file mode 100644 index 0000000..803bf7e --- /dev/null +++ b/titles/sao/data/EquipmentLevel.csv @@ -0,0 +1,121 @@ +EquipmentLevelId,RequireExp +1,200, +2,400, +3,600, +4,800, +5,1000, +6,1200, +7,1400, +8,1600, +9,1800, +10,2000, +11,2200, +12,2400, +13,2600, +14,2800, +15,3000, +16,3200, +17,3400, +18,3600, +19,3800, +20,4000, +21,4200, +22,4400, +23,4600, +24,4800, +25,5000, +26,5200, +27,5400, +28,5600, +29,5800, +30,6000, +31,6200, +32,6400, +33,6600, +34,6800, +35,7000, +36,7200, +37,7400, +38,7600, +39,7800, +40,8000, +41,8200, +42,8400, +43,8600, +44,8800, +45,9000, +46,9200, +47,9400, +48,9600, +49,9800, +50,10000, +51,10200, +52,10400, +53,10600, +54,10800, +55,11000, +56,11200, +57,11400, +58,11600, +59,11800, +60,12000, +61,12200, +62,12400, +63,12600, +64,12800, +65,13000, +66,13200, +67,13400, +68,13600, +69,13800, +70,14000, +71,14200, +72,14400, +73,14600, +74,14800, +75,15000, +76,15200, +77,15400, +78,15600, +79,15800, +80,16000, +81,16200, +82,16400, +83,16600, +84,16800, +85,17000, +86,17200, +87,17400, +88,17600, +89,17800, +90,18000, +91,18200, +92,18400, +93,18600, +94,18800, +95,19000, +96,19200, +97,19400, +98,19600, +99,19800, +100,100000, +101,150000, +102,200000, +103,250000, +104,300000, +105,350000, +106,400000, +107,450000, +108,500000, +109,550000, +110,600000, +111,650000, +112,700000, +113,750000, +114,800000, +115,850000, +116,900000, +117,950000, +118,1000000, +119,1000000, +120,1000000, diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 0c74182..8ed8ba0 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -660,19 +660,57 @@ class SaoGetEquipmentUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, equipmentIdsData) -> None: + def __init__(self, cmd, equipment_data) -> None: super().__init__(cmd) self.result = 1 + + self.user_equipment_id = [] + self.enhancement_value = [] + self.max_enhancement_value_extended_num = [] + self.enhancement_exp = [] + self.awakening_stage = [] + self.awakening_exp = [] + self.possible_awakening_flag = [] + equipment_level = 0 + + for i in range(len(equipment_data)): + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/EquipmentLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = equipment_data[i][4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp Optional[int]: + sql = insert(equipment_data).values( + user=user_id, + equipment_id=equipment_id, + enhancement_value=enhancement_value, + enhancement_exp=enhancement_exp, + awakening_exp=awakening_exp, + awakening_stage=awakening_stage, + possible_awakening_flag=possible_awakening_flag, + ) + + conflict = sql.on_duplicate_key_update( + enhancement_value=enhancement_value, + enhancement_exp=enhancement_exp, + awakening_exp=awakening_exp, + awakening_stage=awakening_stage, + possible_awakening_flag=possible_awakening_flag, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert equipment! user: {user_id}, equipment_id: {equipment_id}" + ) + return None + + return result.lastrowid def put_hero_log(self, user_id: int, user_hero_log_id: int, log_level: int, log_exp: int, main_weapon: int, sub_equipment: int, skill_slot1_skill_id: int, skill_slot2_skill_id: int, skill_slot3_skill_id: int, skill_slot4_skill_id: int, skill_slot5_skill_id: int) -> Optional[int]: sql = insert(hero_log_data).values( @@ -144,6 +192,23 @@ class SaoItemData(BaseData): return None return result.lastrowid + + def get_user_equipments( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all equipments lookup given a profile + """ + sql = equipment_data.select( + and_( + equipment_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def get_hero_log( self, user_id: int, user_hero_log_id: int = None @@ -167,7 +232,7 @@ class SaoItemData(BaseData): self, user_id: int ) -> Optional[List[Row]]: """ - A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + A catch-all hero lookup given a profile """ sql = hero_log_data.select( and_( diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index ca000b8..2635b5f 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -264,6 +264,14 @@ class SaoStaticData(BaseData): return None return [list[2] for list in result.fetchall()] + def get_equipment_id(self, equipmentId: int) -> Optional[Dict]: + sql = equipment.select(equipment.c.equipmentId == equipmentId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: sql = equipment.select(equipment.c.version == version and equipment.c.enabled == enabled).order_by( equipment.c.equipmentId.asc() From 5c3f812caf6c6c187fb5344e6783d86f65dca281 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:30:34 -0400 Subject: [PATCH 238/589] cxb: fix missing parameters on render_POST --- titles/cxb/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 0c38d55..0ef8667 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -103,7 +103,7 @@ class CxbServlet(resource.Resource): else: self.logger.info(f"Ready on port {self.game_cfg.server.port}") - def render_POST(self, request: Request): + def render_POST(self, request: Request, version: int, endpoint: str): version = 0 internal_ver = 0 func_to_find = "" From 2418abaccec7e83d8c532e5cf335c767d3a760bd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:31:09 -0400 Subject: [PATCH 239/589] title: convert version to int to match POST endpoint --- core/title.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/title.py b/core/title.py index 7a0a99b..ab53773 100644 --- a/core/title.py +++ b/core/title.py @@ -84,7 +84,7 @@ class TitleServlet: request.setResponseCode(405) return b"" - return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) + return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"]) def render_POST(self, request: Request, endpoints: dict) -> bytes: code = endpoints["game"] From 37d24b3b4dec072ead98dcdd4e1b3c606ee11856 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:32:27 -0400 Subject: [PATCH 240/589] mucha: now respects log level set in core.yaml --- core/mucha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mucha.py b/core/mucha.py index a90ab53..eecae3a 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -33,8 +33,8 @@ class MuchaServlet: 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) + self.logger.setLevel(cfg.mucha.loglevel) + coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str) all_titles = Utils.get_all_titles() From 20865dc4950f7f464da065c94eabcb218aac165d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:45:37 -0400 Subject: [PATCH 241/589] allnet: add logging --- core/allnet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/allnet.py b/core/allnet.py index 32ae177..bd2dfaf 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -225,6 +225,7 @@ class AllnetServlet: req_file = match["file"].replace("%0A", "") if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): + self.logger.info(f"Request for DL INI file {req_file} successful") return open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" ).read() From ac9e71ee2f1a74a5b9171d851b2e5594854c8518 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:46:26 -0400 Subject: [PATCH 242/589] hotfix allnet logging --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index bd2dfaf..160f818 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -225,7 +225,7 @@ class AllnetServlet: req_file = match["file"].replace("%0A", "") if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): - self.logger.info(f"Request for DL INI file {req_file} successful") + self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") return open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" ).read() From db77e61b7987ff1b0e824e3fa4729750daf27aaa Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 30 May 2023 21:52:21 -0400 Subject: [PATCH 243/589] allnet: add event logging --- core/allnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 160f818..8af120b 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -216,6 +216,7 @@ class AllnetServlet: resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" self.logger.debug(f"Sending download uri {resp.uri}") + self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") return self.dict_to_http_form_string([vars(resp)]) def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: @@ -226,6 +227,7 @@ class AllnetServlet: if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") + self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") return open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" ).read() From cf6cfdbd3bb97dbb461a4d7a57a93cccba5b0682 Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 31 May 2023 21:58:30 -0400 Subject: [PATCH 244/589] adding partial synthetize system for SAO --- titles/sao/base.py | 71 +++++++++++++++++++ titles/sao/handlers/base.py | 137 ++++++++++++++++++++++++++++++++++++ titles/sao/schema/item.py | 37 +++++++++- titles/sao/schema/static.py | 8 +++ 4 files changed, 252 insertions(+), 1 deletion(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 543f488..ac1b69c 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -244,6 +244,77 @@ class SaoBase: resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() + def handle_c816(self, request: Any) -> bytes: # not fully done yet + #custom/synthesize_enhancement_equipment + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "origin_user_equipment_id_size" / Rebuild(Int32ub, len_(this.origin_user_equipment_id) * 2), # calculates the length of the origin_user_equipment_id + "origin_user_equipment_id" / PaddedString(this.origin_user_equipment_id_size, "utf_16_le"), # origin_user_equipment_id is a (zero) padded string + Padding(3), + "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, + "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( + "common_reward_type" / Int16ub, # team_no is a byte + "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id + "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string + )), + ) + + req_data = req_struct.parse(req) + + user_id = req_data.user_id + synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + + for i in range(0,req_data.material_common_reward_user_data_list_length): + + itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if itemList: + equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) + # Then delete the used item, function for items progression is not done yet... + + if equipmentList: + equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) + self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if heroList: + hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) + self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + self.game_data.item.put_equipment_data(req_data.user_id, int(req_data.origin_user_equipment_id), synthesize_equipment_data["enhancement_value"], equipment_exp, 0, 0, 0) + + profile = self.game_data.profile.get_profile(req_data.user_id) + new_col = int(profile["own_col"]) - 100 + + # Update profile + + self.game_data.profile.put_profile( + req_data.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + profile["rank_exp"], + new_col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Load the item again to push to the response handler + synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + + resp = SaoSynthesizeEnhancementEquipmentResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_equipment_data) + return resp.make() + def handle_c806(self, request: Any) -> bytes: #custom/change_party req = bytes.fromhex(request)[24:] diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 8ed8ba0..4949143 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1773,5 +1773,142 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): )) + self.length = len(resp_data) + return super().make() + resp_data + +class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): + def __init__(self, cmd, synthesize_equipment_data) -> None: + super().__init__(cmd) + self.result = 1 + equipment_level = 0 + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/EquipmentLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = synthesize_equipment_data[4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: + + after_equipment_user_data_struct = Struct( + "user_equipment_id_size" / Int32ub, # big endian + "user_equipment_id" / Int16ul[9], #string + "equipment_id" / Int32ub, #int + "enhancement_value" / Int16ub, #short + "max_enhancement_value_extended_num" / Int16ub, #short + "enhancement_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "after_equipment_user_data_size" / Rebuild(Int32ub, len_(this.after_equipment_user_data)), # big endian + "after_equipment_user_data" / Array(this.after_equipment_user_data_size, after_equipment_user_data_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + after_equipment_user_data_size=0, + after_equipment_user_data=[], + ))) + + synthesize_equipment_data = dict( + user_equipment_id_size=len(self.user_equipment_id) * 2, + user_equipment_id=[ord(x) for x in self.user_equipment_id], + equipment_id=self.equipment_id, + enhancement_value=self.enhancement_value, + max_enhancement_value_extended_num=self.max_enhancement_value_extended_num, + enhancement_exp=self.enhancement_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.after_equipment_user_data.append(synthesize_equipment_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + self.length = len(resp_data) return super().make() + resp_data \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index e1cb207..0e3d86c 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -192,6 +192,14 @@ class SaoItemData(BaseData): return None return result.lastrowid + + def get_user_equipment(self, user_id: int, equipment_id: int) -> Optional[Dict]: + sql = equipment_data.select(equipment_data.c.user == user_id and equipment_data.c.equipment_id == equipment_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() def get_user_equipments( self, user_id: int @@ -274,4 +282,31 @@ class SaoItemData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() \ No newline at end of file + return result.fetchone() + + def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: + sql = hero_log_data.delete( + and_( + hero_log_data.c.user == user_id, + hero_log_data.c.user_hero_log_id == user_hero_log_id, + ) + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to remove hero log! profile: {user_id}, user_hero_log_id: {user_hero_log_id}" + ) + return None + + def remove_equipment(self, user_id: int, equipment_id: int) -> None: + sql = equipment_data.delete( + and_(equipment_data.c.user == user_id, equipment_data.c.equipment_id == equipment_id) + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to remove equipment! profile: {user_id}, equipment_id: {equipment_id}" + ) + return None \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index 2635b5f..7323fc8 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -281,6 +281,14 @@ class SaoStaticData(BaseData): if result is None: return None return [list[2] for list in result.fetchall()] + + def get_item_id(self, itemId: int) -> Optional[Dict]: + sql = item.select(item.c.itemId == itemId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by( From 3bd03c592edbf22fed8cc3f1f75075bbb0649878 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 1 Jun 2023 13:19:48 -0400 Subject: [PATCH 245/589] Item progression and synthesize of hero and equipment done --- docs/game_specific_info.md | 6 ++ titles/sao/base.py | 107 +++++++++++++++++++++-- titles/sao/handlers/base.py | 166 +++++++++++++++++++++++++++++++++++- titles/sao/schema/item.py | 63 ++++++++++++++ 4 files changed, 334 insertions(+), 8 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index aa0a39f..15efe7c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -436,6 +436,12 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core python dbutils.py --game SDEW upgrade ``` +### Notes +- Stages are currently force unlocked, no progression +- Co-Op (matching) is not supported +- Shop is not functionnal +- Player title is currently static and cannot be changed in-game + ### Credits for SAO support: - Midorica - Limited Network Support diff --git a/titles/sao/base.py b/titles/sao/base.py index ac1b69c..982e256 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -165,9 +165,21 @@ class SaoBase: def handle_c604(self, request: Any) -> bytes: #have_object/get_item_user_data_list - itemIdsData = self.game_data.static.get_item_ids(0, True) - - resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, itemIdsData) + #itemIdsData = self.game_data.static.get_item_ids(0, True) + req = bytes.fromhex(request)[24:] + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + item_data = self.game_data.item.get_user_items(user_id) + + resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data) + #resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() def handle_c606(self, request: Any) -> bytes: @@ -244,7 +256,89 @@ class SaoBase: resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - def handle_c816(self, request: Any) -> bytes: # not fully done yet + def handle_c814(self, request: Any) -> bytes: + #custom/synthesize_enhancement_hero_log + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "origin_user_hero_log_id_size" / Rebuild(Int32ub, len_(this.origin_user_hero_log_id) * 2), # calculates the length of the origin_user_hero_log_id + "origin_user_hero_log_id" / PaddedString(this.origin_user_hero_log_id_size, "utf_16_le"), # origin_user_hero_log_id is a (zero) padded string + Padding(3), + "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, + "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( + "common_reward_type" / Int16ub, # team_no is a byte + "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id + "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string + )), + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + + for i in range(0,req_data.material_common_reward_user_data_list_length): + + itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if itemList: + hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if equipmentList: + equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if heroList: + hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + self.game_data.item.put_hero_log( + user_id, + int(req_data.origin_user_hero_log_id), + synthesize_hero_log_data["log_level"], + hero_exp, + synthesize_hero_log_data["main_weapon"], + synthesize_hero_log_data["sub_equipment"], + synthesize_hero_log_data["skill_slot1_skill_id"], + synthesize_hero_log_data["skill_slot2_skill_id"], + synthesize_hero_log_data["skill_slot3_skill_id"], + synthesize_hero_log_data["skill_slot4_skill_id"], + synthesize_hero_log_data["skill_slot5_skill_id"] + ) + + profile = self.game_data.profile.get_profile(req_data.user_id) + new_col = int(profile["own_col"]) - 100 + + # Update profile + + self.game_data.profile.put_profile( + req_data.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + profile["rank_exp"], + new_col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Load the item again to push to the response handler + synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + + resp = SaoSynthesizeEnhancementHeroLogResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_hero_log_data) + return resp.make() + + def handle_c816(self, request: Any) -> bytes: #custom/synthesize_enhancement_equipment req = bytes.fromhex(request)[24:] @@ -278,7 +372,7 @@ class SaoBase: if itemList: equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) - # Then delete the used item, function for items progression is not done yet... + self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) if equipmentList: equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) @@ -595,10 +689,13 @@ class SaoBase: heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) + itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) if heroList: self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: self.game_data.item.put_equipment_data(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) + if itemList: + self.game_data.item.put_item(req_data.user_id, randomized_unanalyzed_id['CommonRewardId']) # Send response diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 4949143..91b30fe 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -816,13 +816,18 @@ class SaoGetItemUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetItemUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, itemIdsData) -> None: + def __init__(self, cmd, item_data) -> None: super().__init__(cmd) self.result = 1 + self.user_item_id = [] + + for i in range(len(item_data)): + self.user_item_id.append(item_data[i][2]) + # item_user_data_list - self.user_item_id = list(map(str,itemIdsData)) #str - self.item_id = itemIdsData #int + self.user_item_id = list(map(str,self.user_item_id)) #str + self.item_id = list(map(int,self.user_item_id)) #int self.protect_flag = 0 #byte self.get_date = "20230101120000" #str @@ -1776,6 +1781,161 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): + def __init__(self, cmd, hero_data) -> None: + super().__init__(cmd) + self.result = 1 + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = hero_data[4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: + #new stuff + + hero_log_user_data_list_struct = Struct( + "user_hero_log_id_size" / Int32ub, # big endian + "user_hero_log_id" / Int16ul[9], #string + "hero_log_id" / Int32ub, #int + "log_level" / Int16ub, #short + "max_log_level_extended_num" / Int16ub, #short + "log_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 + "last_set_skill_slot1_skill_id" / Int16ub, #short + "last_set_skill_slot2_skill_id" / Int16ub, #short + "last_set_skill_slot3_skill_id" / Int16ub, #short + "last_set_skill_slot4_skill_id" / Int16ub, #short + "last_set_skill_slot5_skill_id" / Int16ub, #short + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian + "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + hero_log_user_data_list_size=0, + hero_log_user_data_list=[], + ))) + + hero_data = dict( + user_hero_log_id_size=len(self.user_hero_log_id) * 2, + user_hero_log_id=[ord(x) for x in self.user_hero_log_id], + hero_log_id=self.hero_log_id, + log_level=self.log_level, + max_log_level_extended_num=self.max_log_level_extended_num, + log_exp=self.log_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + skill_slot_correction_value=self.skill_slot_correction_value, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.hero_log_user_data_list.append(hero_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 0e3d86c..858267e 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -28,6 +28,21 @@ equipment_data = Table( mysql_charset="utf8mb4", ) +item_data = Table( + "sao_item_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("item_id", Integer, nullable=False), + Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "item_id", name="sao_item_data_uk"), + mysql_charset="utf8mb4", +) + hero_log_data = Table( "sao_hero_log_data", metadata, @@ -104,6 +119,25 @@ class SaoItemData(BaseData): self.logger.error(f"Failed to create SAO session for user {user_id}!") return None return result.lastrowid + + def put_item(self, user_id: int, item_id: int) -> Optional[int]: + sql = insert(item_data).values( + user=user_id, + item_id=item_id, + ) + + conflict = sql.on_duplicate_key_update( + item_id=item_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}" + ) + return None + + return result.lastrowid def put_equipment_data(self, user_id: int, equipment_id: int, enhancement_value: int, enhancement_exp: int, awakening_exp: int, awakening_stage: int, possible_awakening_flag: int) -> Optional[int]: sql = insert(equipment_data).values( @@ -218,6 +252,23 @@ class SaoItemData(BaseData): return None return result.fetchall() + def get_user_items( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all items lookup given a profile + """ + sql = item_data.select( + and_( + item_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_hero_log( self, user_id: int, user_hero_log_id: int = None ) -> Optional[List[Row]]: @@ -309,4 +360,16 @@ class SaoItemData(BaseData): self.logger.error( f"{__name__} failed to remove equipment! profile: {user_id}, equipment_id: {equipment_id}" ) + return None + + def remove_item(self, user_id: int, item_id: int) -> None: + sql = item_data.delete( + and_(item_data.c.user == user_id, item_data.c.item_id == item_id) + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to remove item! profile: {user_id}, item_id: {item_id}" + ) return None \ No newline at end of file From 84fc002cdbb882a547d17fb4b91c3e48f54332a2 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 2 Jun 2023 13:53:49 -0400 Subject: [PATCH 246/589] Adding trial tower support for SAO --- titles/sao/base.py | 186 ++++++++++++++++++++++++++++++++++++ titles/sao/handlers/base.py | 126 ++++++++++++++++++++++++ 2 files changed, 312 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 982e256..ef3879a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -711,7 +711,193 @@ class SaoBase: resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) return resp.make() + def handle_c918(self, request: Any) -> bytes: + #quest/trial_tower_play_end + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + Padding(2), + "trial_tower_id" / Int16ub, # trial_tower_id is a short, + Padding(3), + "play_end_request_data" / Int8ub, # play_end_request_data is a byte + Padding(1), + "play_result_flag" / Int8ub, # play_result_flag is a byte + Padding(2), + "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, + "base_get_data" / Array(this.base_get_data_length, Struct( + "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int + "get_col" / Int32ub, # get_num is a short + )), + Padding(3), + "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte + "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( + "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int + )), + Padding(3), + "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte + "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( + "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int + )), + Padding(3), + "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte + "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( + "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int + )), + Padding(3), + "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte + "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( + "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, + )), + Padding(3), + "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, + "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( + "event_item_id" / Int32ub, # event_item_id is an int + "get_num" / Int16ub, # get_num is a short + )), + Padding(3), + "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte + "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte + "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( + "boss_type" / Int8ub, # boss_type is a byte + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte + "mission_data_list" / Array(this.mission_data_list_length, Struct( + "mission_id" / Int32ub, # enemy_kind_id is an int + "clear_flag" / Int8ub, # boss_type is a byte + "mission_difficulty_id" / Int16ub, # destroy_num is a short + )), + Padding(3), + "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte + "score_data" / Array(this.score_data_length, Struct( + "clear_time" / Int32ub, # clear_time is an int + "combo_num" / Int32ub, # boss_type is a int + "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage + "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string + "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short + "reaching_skill_level" / Int16ub, # reaching_skill_level is a short + "ko_chara_num" / Int8ub, # ko_chara_num is a byte + "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short + "boss_destroying_num" / Int16ub, # boss_destroying_num is a short + "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte + "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int + "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte + "continue_cnt" / Int16ub, # continue_cnt is a short + "total_loss_num" / Int16ub, # total_loss_num is a short + )), + + ) + + req_data = req_struct.parse(req) + + # Update the profile + profile = self.game_data.profile.get_profile(req_data.user_id) + + exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason + col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/PlayerRank.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for i in range(0,len(data)): + if exp>=int(data[i][1]) and exp bytes: #should be tweaked for proper item unlock #quest/episode_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() + + def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode + #quest/trial_tower_play_end_unanalyzed_log_fixed + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 91b30fe..a9d336a 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1530,6 +1530,132 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoTrialTowerPlayEndRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoTrialTowerPlayEndResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_response_data_size = 1 # Number of arrays + self.multi_play_end_response_data_size = 1 # Unused on solo play + self.trial_tower_play_end_updated_notification_data_size = 1 # Number of arrays + self.treasure_hunt_play_end_response_data_size = 1 # Number of arrays + + self.dummy_1 = 0 + self.dummy_2 = 0 + self.dummy_3 = 0 + + self.rarity_up_occurrence_flag = 0 + self.adventure_ex_area_occurrences_flag = 0 + self.ex_bonus_data_list_size = 1 # Number of arrays + self.play_end_player_trace_reward_data_list_size = 0 # Number of arrays + + self.ex_bonus_table_id = 0 # ExBonusTable.csv values, dont care for now + self.achievement_status = 1 + + self.common_reward_data_size = 1 # Number of arrays + + self.common_reward_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv + self.common_reward_id = 0 + self.common_reward_num = 0 + + self.store_best_score_clear_time_flag = 0 + self.store_best_score_combo_num_flag = 0 + self.store_best_score_total_damage_flag = 0 + self.store_best_score_concurrent_destroying_num_flag = 0 + self.store_reaching_trial_tower_rank = 0 + + self.get_event_point = 0 + self.total_event_point = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_end_response_data_size" / Int32ub, # big endian + + "rarity_up_occurrence_flag" / Int8ul, # result is either 0 or 1 + "adventure_ex_area_occurrences_flag" / Int8ul, # result is either 0 or 1 + "ex_bonus_data_list_size" / Int32ub, # big endian + "play_end_player_trace_reward_data_list_size" / Int32ub, # big endian + + # ex_bonus_data_list + "ex_bonus_table_id" / Int32ub, + "achievement_status" / Int8ul, # result is either 0 or 1 + + # play_end_player_trace_reward_data_list + "common_reward_data_size" / Int32ub, + + # common_reward_data + "common_reward_type" / Int16ub, # short + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + + "multi_play_end_response_data_size" / Int32ub, # big endian + + # multi_play_end_response_data + "dummy_1" / Int8ul, # result is either 0 or 1 + "dummy_2" / Int8ul, # result is either 0 or 1 + "dummy_3" / Int8ul, # result is either 0 or 1 + + "trial_tower_play_end_updated_notification_data_size" / Int32ub, # big endian + + #trial_tower_play_end_updated_notification_data + "store_best_score_clear_time_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_combo_num_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_total_damage_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_concurrent_destroying_num_flag" / Int8ul, # result is either 0 or 1 + "store_reaching_trial_tower_rank" / Int32ub, + + "treasure_hunt_play_end_response_data_size" / Int32ub, # big endian + + #treasure_hunt_play_end_response_data + "get_event_point" / Int32ub, + "total_event_point" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_end_response_data_size=self.play_end_response_data_size, + + rarity_up_occurrence_flag=self.rarity_up_occurrence_flag, + adventure_ex_area_occurrences_flag=self.adventure_ex_area_occurrences_flag, + ex_bonus_data_list_size=self.ex_bonus_data_list_size, + play_end_player_trace_reward_data_list_size=self.play_end_player_trace_reward_data_list_size, + + ex_bonus_table_id=self.ex_bonus_table_id, + achievement_status=self.achievement_status, + + common_reward_data_size=self.common_reward_data_size, + + common_reward_type=self.common_reward_type, + common_reward_id=self.common_reward_id, + common_reward_num=self.common_reward_num, + + multi_play_end_response_data_size=self.multi_play_end_response_data_size, + + dummy_1=self.dummy_1, + dummy_2=self.dummy_2, + dummy_3=self.dummy_3, + + trial_tower_play_end_updated_notification_data_size=self.trial_tower_play_end_updated_notification_data_size, + store_best_score_clear_time_flag=self.store_best_score_clear_time_flag, + store_best_score_combo_num_flag=self.store_best_score_combo_num_flag, + store_best_score_total_damage_flag=self.store_best_score_total_damage_flag, + store_best_score_concurrent_destroying_num_flag=self.store_best_score_concurrent_destroying_num_flag, + store_reaching_trial_tower_rank=self.store_reaching_trial_tower_rank, + + treasure_hunt_play_end_response_data_size=self.treasure_hunt_play_end_response_data_size, + + get_event_point=self.get_event_point, + total_event_point=self.total_event_point, + )) + + self.length = len(resp_data) + return super().make() + resp_data + class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) From a0b25e2b7b2cc687e5066ef69a1c4b4d4199c167 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 3 Jun 2023 11:42:50 -0400 Subject: [PATCH 247/589] Adding rare drops saving to SAO --- titles/sao/base.py | 33 ++++++++++++++++++++++++++++++- titles/sao/read.py | 24 +++++++++++++++++++++++ titles/sao/schema/static.py | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index ef3879a..098b9c3 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -675,6 +675,22 @@ class SaoBase: hero_data["skill_slot4_skill_id"], hero_data["skill_slot5_skill_id"] ) + + # Grab the rare loot from the table, match it with the right item and then push to the player profile + for r in range(0,req_data.get_rare_drop_data_list_length): + rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + commonRewardId = rewardList["commonRewardId"] + + heroList = self.game_data.static.get_hero_id(commonRewardId) + equipmentList = self.game_data.static.get_equipment_id(commonRewardId) + itemList = self.game_data.static.get_item_id(commonRewardId) + + if heroList: + self.game_data.item.put_hero_log(req_data.user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + if equipmentList: + self.game_data.item.put_equipment_data(req_data.user_id, commonRewardId, 1, 200, 0, 0, 0) + if itemList: + self.game_data.item.put_item(req_data.user_id, commonRewardId) # Generate random hero(es) based off the response for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): @@ -825,7 +841,6 @@ class SaoBase: player_level = int(data[i][0]) break - # Update profile updated_profile = self.game_data.profile.put_profile( req_data.user_id, profile["user_type"], @@ -866,6 +881,22 @@ class SaoBase: hero_data["skill_slot5_skill_id"] ) + # Grab the rare loot from the table, match it with the right item and then push to the player profile + for r in range(0,req_data.get_rare_drop_data_list_length): + rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + commonRewardId = rewardList["commonRewardId"] + + heroList = self.game_data.static.get_hero_id(commonRewardId) + equipmentList = self.game_data.static.get_equipment_id(commonRewardId) + itemList = self.game_data.static.get_item_id(commonRewardId) + + if heroList: + self.game_data.item.put_hero_log(req_data.user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + if equipmentList: + self.game_data.item.put_equipment_data(req_data.user_id, commonRewardId, 1, 200, 0, 0, 0) + if itemList: + self.game_data.item.put_item(req_data.user_id, commonRewardId) + # Generate random hero(es) based off the response for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): diff --git a/titles/sao/read.py b/titles/sao/read.py index 5fc9804..d70c275 100644 --- a/titles/sao/read.py +++ b/titles/sao/read.py @@ -228,3 +228,27 @@ class SaoReader(BaseReader): continue except: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading RareDropTable.csv") + try: + fullPath = bin_dir + "/RareDropTable.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + questRareDropId = row["QuestRareDropId"] + commonRewardId = row["CommonRewardId"] + enabled = True + + self.logger.info(f"Added rare drop {questRareDropId} | Reward: {commonRewardId}") + + try: + self.data.static.put_rare_drop( + 0, + questRareDropId, + commonRewardId, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index 7323fc8..670e3b2 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -96,6 +96,20 @@ support = Table( mysql_charset="utf8mb4", ) +rare_drop = Table( + "sao_static_rare_drop_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("questRareDropId", Integer), + Column("commonRewardId", Integer), + Column("enabled", Boolean), + UniqueConstraint( + "version", "questRareDropId", "commonRewardId", name="sao_static_rare_drop_list_uk" + ), + mysql_charset="utf8mb4", +) + title = Table( "sao_static_title_list", metadata, @@ -215,6 +229,23 @@ class SaoStaticData(BaseData): if result is None: return None return result.lastrowid + + def put_rare_drop( self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool ) -> Optional[int]: + sql = insert(rare_drop).values( + version=version, + questRareDropId=questRareDropId, + commonRewardId=commonRewardId, + enabled=enabled, + ) + + conflict = sql.on_duplicate_key_update( + questRareDropId=questRareDropId, commonRewardId=commonRewardId, version=version + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]: sql = insert(title).values( @@ -289,6 +320,14 @@ class SaoStaticData(BaseData): if result is None: return None return result.fetchone() + + def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]: + sql = rare_drop.select(rare_drop.c.questRareDropId == questRareDropId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by( From 5ca16f206733e1fb0c5233895c53fdf161aa34df Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 13 Jun 2023 22:07:48 -0400 Subject: [PATCH 248/589] mai2: fix GetUserMusicApi pagination --- titles/mai2/base.py | 31 ++++++++++++++++++++----------- titles/mai2/schema/score.py | 2 ++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 44ec60d..1bba918 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -646,21 +646,30 @@ class Mai2Base: return {"userId": data["userId"], "length": 0, "userRegionList": []} def handle_get_user_music_api_request(self, data: Dict) -> Dict: - songs = self.data.score.get_best_scores(data["userId"]) + songs = self.data.score.get_best_scores(data.get("userId", 0)) + if songs is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMusicList": [], + } + music_detail_list = [] - next_index = 0 + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 50) + upper_lim = next_index + max_ct + num_user_songs = len(songs) - if songs is not None: - for song in songs: - tmp = song._asdict() - tmp.pop("id") - tmp.pop("user") - music_detail_list.append(tmp) + for x in range(next_index, upper_lim): + if num_user_songs >= x: + break - if len(music_detail_list) == data["maxCount"]: - next_index = data["maxCount"] + data["nextIndex"] - break + tmp = songs[x]._asdict() + tmp.pop("id") + tmp.pop("user") + music_detail_list.append(tmp) + next_index = 0 if len(music_detail_list) < max_ct else upper_lim return { "userId": data["userId"], "nextIndex": next_index, diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 0f7f239..85dff16 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from core.data import cached best_score = Table( "mai2_score_best", @@ -190,6 +191,7 @@ class Mai2ScoreData(BaseData): return None return result.lastrowid + @cached(2) def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: sql = best_score.select( and_( From 5a35b1c82396855aa6beb31cdcc879f33fd70e60 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 13 Jun 2023 22:10:35 -0400 Subject: [PATCH 249/589] mai2: GetUserMusicApi hotfix --- titles/mai2/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 1bba918..13ea2df 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -660,6 +660,7 @@ class Mai2Base: upper_lim = next_index + max_ct num_user_songs = len(songs) + for x in range(next_index, upper_lim): if num_user_songs >= x: break @@ -669,7 +670,7 @@ class Mai2Base: tmp.pop("user") music_detail_list.append(tmp) - next_index = 0 if len(music_detail_list) < max_ct else upper_lim + next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim return { "userId": data["userId"], "nextIndex": next_index, From f56332141e838a92349c449d057c7162db198d4c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 13 Jun 2023 22:16:30 -0400 Subject: [PATCH 250/589] mai2: fix old server (finale isn't ready yet) --- titles/mai2/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 13ea2df..6d14bce 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -18,10 +18,10 @@ class Mai2Base: self.logger = logging.getLogger("mai2") if self.core_config.server.is_develop and self.core_config.title.port > 0: - self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEZ/100/" else: - self.old_server = f"http://{self.core_config.title.hostname}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/" def handle_get_game_setting_api_request(self, data: Dict): # TODO: See if making this epoch 0 breaks things From 65686fb615d94536bfc0d7922fe44c222f00f9b3 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 13 Jun 2023 22:35:09 -0400 Subject: [PATCH 251/589] mai2: add loggin to handle_get_user_music_api_request --- titles/mai2/base.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 6d14bce..6ad36d7 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -646,21 +646,27 @@ class Mai2Base: return {"userId": data["userId"], "length": 0, "userRegionList": []} def handle_get_user_music_api_request(self, data: Dict) -> Dict: - songs = self.data.score.get_best_scores(data.get("userId", 0)) + user_id = data.get("userId", 0) + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 50) + upper_lim = next_index + max_ct + music_detail_list = [] + + if user_id <= 0: + self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") + return {} + + songs = self.data.score.get_best_scores(user_id) if songs is None: + self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { "userId": data["userId"], "nextIndex": 0, "userMusicList": [], } - music_detail_list = [] - next_index = data.get("nextIndex", 0) - max_ct = data.get("maxCount", 50) - upper_lim = next_index + max_ct num_user_songs = len(songs) - for x in range(next_index, upper_lim): if num_user_songs >= x: break @@ -671,6 +677,7 @@ class Mai2Base: music_detail_list.append(tmp) next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") return { "userId": data["userId"], "nextIndex": next_index, From 1b2f5e3709739061a740427bd1912a327bc9cbe1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 13 Jun 2023 22:50:57 -0400 Subject: [PATCH 252/589] mai2: fix logic in handle_get_user_music_api_request --- titles/mai2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 6ad36d7..6f32518 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -668,7 +668,7 @@ class Mai2Base: num_user_songs = len(songs) for x in range(next_index, upper_lim): - if num_user_songs >= x: + if num_user_songs <= x: break tmp = songs[x]._asdict() From b12938bcd83dcddf8eee7588939c13fb7c72b628 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 14 Jun 2023 03:00:52 -0400 Subject: [PATCH 253/589] pokken: add partial profile save logic --- titles/pokken/base.py | 56 +++++++++++++- titles/pokken/const.py | 12 +-- titles/pokken/schema/item.py | 14 +++- titles/pokken/schema/profile.py | 126 ++++++++++++++++++++++++++++---- 4 files changed, 184 insertions(+), 24 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 40e6444..3ec4475 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta import json, logging -from typing import Any, Dict +from typing import Any, Dict, List import random from core.data import Data @@ -274,6 +274,60 @@ class PokkenBase: res.result = 1 res.type = jackal_pb2.MessageType.SAVE_USER + req = request.save_user + user_id = req.banapass_id + + tut_flgs: List[int] = [] + ach_flgs: List[int] = [] + evt_flgs: List[int] = [] + evt_params: List[int] = [] + + get_rank_pts: int = req.get_trainer_rank_point if req.get_trainer_rank_point else 0 + get_money: int = req.get_money + get_score_pts: int = req.get_score_point if req.get_score_point else 0 + grade_max: int = req.grade_max_num + extra_counter: int = req.extra_counter + evt_reward_get_flg: int = req.event_reward_get_flag + num_continues: int = req.continue_num + total_play_days: int = req.total_play_days + awake_num: int = req.awake_num # ? + use_support_ct: int = req.use_support_num + beat_num: int = req.beat_num # ? + evt_state: int = req.event_state + aid_skill: int = req.aid_skill + last_evt: int = req.last_play_event_id + + battle = req.battle_data + mon = req.pokemon_data + + self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) + self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) + self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + + if req.trainer_name_pending: # we're saving for the first time + self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None) + + for tut_flg in req.tutorial_progress_flag: + tut_flgs.append(tut_flg) + + self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs) + + for ach_flg in req.achievement_flag: + ach_flgs.append(ach_flg) + + self.data.profile.update_profile_tutorial_flags(user_id, ach_flg) + + for evt_flg in req.event_achievement_flag: + evt_flgs.append(evt_flg) + + for evt_param in req.event_achievement_param: + evt_params.append(evt_param) + + self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, ) + + for reward in req.reward_data: + self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) + return res.SerializeToString() def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 2eb5357..e7ffdd8 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -11,14 +11,14 @@ class PokkenConstants: VERSION_NAMES = "Pokken Tournament" class BATTLE_TYPE(Enum): - BATTLE_TYPE_TUTORIAL = 1 - BATTLE_TYPE_AI = 2 - BATTLE_TYPE_LAN = 3 - BATTLE_TYPE_WAN = 4 + TUTORIAL = 1 + AI = 2 + LAN = 3 + WAN = 4 class BATTLE_RESULT(Enum): - BATTLE_RESULT_WIN = 1 - BATTLE_RESULT_LOSS = 2 + WIN = 1 + LOSS = 2 @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py index 4919ea0..32bff2a 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -31,4 +31,16 @@ class PokkenItemData(BaseData): Items obtained as rewards """ - pass + def add_reward(self, user_id: int, category: int, content: int, item_type: int) -> Optional[int]: + sql = insert(item).values( + user=user_id, + category=category, + content=content, + type=item_type, + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}") + return None + return result.lastrowid diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 8e536f1..94e15e8 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Union from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey @@ -125,7 +125,7 @@ pokemon_data = Table( Column("win_vs_lan", Integer), Column("battle_num_vs_cpu", Integer), # 2 Column("win_cpu", Integer), - Column("battle_all_num_tutorial", Integer), + Column("battle_all_num_tutorial", Integer), # ??? Column("battle_num_tutorial", Integer), # 1? Column("bp_point_atk", Integer), Column("bp_point_res", Integer), @@ -147,11 +147,10 @@ class PokkenProfileData(BaseData): return None return result.lastrowid - def set_profile_name(self, user_id: int, new_name: str) -> None: - sql = ( - update(profile) - .where(profile.c.user == user_id) - .values(trainer_name=new_name) + def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + trainer_name=new_name, + avatar_gender=gender if gender is not None else profile.c.avatar_gender ) result = self.execute(sql) if result is None: @@ -159,8 +158,38 @@ class PokkenProfileData(BaseData): f"Failed to update pokken profile name for user {user_id}!" ) - def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: - pass + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + tutorial_progress_flag=tutorial_flags, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile tutorial flags for user {user_id}!" + ) + + def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + achievement_flag=achievement_flags, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile achievement flags for user {user_id}!" + ) + + def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + event_state=event_state, + event_achievement_flag=event_flags, + event_achievement_param=event_param, + last_play_event_id=last_evt if last_evt is not None else profile.c.last_play_event_id, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile event state for user {user_id}!" + ) def add_profile_points( self, user_id: int, rank_pts: int, money: int, score_pts: int @@ -174,18 +203,53 @@ class PokkenProfileData(BaseData): return None return result.fetchone() - def put_pokemon_data( + def put_pokemon( self, user_id: int, pokemon_id: int, illust_no: int, - get_exp: int, atk: int, res: int, defe: int, - sp: int, + sp: int ) -> Optional[int]: - pass + sql = insert(pokemon_data).values( + user=user_id, + char_id=pokemon_id, + illustration_book_no=illust_no, + bp_point_atk=atk, + bp_point_res=res, + bp_point_defe=defe, + bp_point_sp=sp, + ) + + conflict = sql.on_duplicate_key_update( + illustration_book_no=illust_no, + bp_point_atk=atk, + bp_point_res=res, + bp_point_defe=defe, + bp_point_sp=sp, + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}") + return None + return result.lastrowid + + def add_pokemon_xp( + self, + user_id: int, + pokemon_id: int, + xp: int + ) -> None: + sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( + pokemon_exp=pokemon_data.c.pokemon_exp + xp + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}") def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: pass @@ -193,13 +257,29 @@ class PokkenProfileData(BaseData): def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: pass - def put_results( - self, user_id: int, pokemon_id: int, match_type: int, match_result: int + def put_pokemon_battle_result( + self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT ) -> None: """ Records the match stats (type and win/loss) for the pokemon and profile """ - pass + sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( + battle_num_tutorial=pokemon_data.c.battle_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_num_tutorial, + battle_all_num_tutorial=pokemon_data.c.battle_all_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_all_num_tutorial, + + battle_num_vs_cpu=pokemon_data.c.battle_num_vs_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else pokemon_data.c.battle_num_vs_cpu, + win_cpu=pokemon_data.c.win_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_cpu, + + battle_num_vs_lan=pokemon_data.c.battle_num_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else pokemon_data.c.battle_num_vs_lan, + win_vs_lan=pokemon_data.c.win_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_lan, + + battle_num_vs_wan=pokemon_data.c.battle_num_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else pokemon_data.c.battle_num_vs_wan, + win_vs_wan=pokemon_data.c.win_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_wan, + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})") def put_stats( self, @@ -215,3 +295,17 @@ class PokkenProfileData(BaseData): Records profile stats """ pass + + def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None: + sql = update(profile).where(profile.c.user==user_id).values( + support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1, + support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2, + support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1, + support_set_2_2=support2 if support_id == 2 else profile.c.support_set_2_2, + support_set_3_1=support1 if support_id == 3 else profile.c.support_set_3_1, + support_set_3_2=support2 if support_id == 3 else profile.c.support_set_3_2, + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to update support team {support_id} for user {user_id}") From 3c385f505be8a0a99d78ef0054bc504c127d99e9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 23 Jun 2023 00:30:25 -0400 Subject: [PATCH 254/589] pokken: add requirement for autobahn, add stun, turn and admission servers --- requirements.txt | 1 + titles/pokken/base.py | 21 ++++++++++-- titles/pokken/index.py | 26 +++++++++++--- titles/pokken/services.py | 72 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 titles/pokken/services.py diff --git a/requirements.txt b/requirements.txt index ebb17c9..6d37728 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ Routes bcrypt jinja2 protobuf +autobahn diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 3ec4475..306532d 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -357,15 +357,32 @@ class PokkenBase: ) -> Dict: """ "sessionId":"12345678", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, + "list":[] + """ + return { + "data": { + "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 {} + + def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + self.logger.info(f"Admission: JoinSession from {req_ip}") + return { + 'data': { + "id": 123 + } + } \ No newline at end of file diff --git a/titles/pokken/index.py b/titles/pokken/index.py index bccdcaf..1140d41 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,6 +1,8 @@ from typing import Tuple from twisted.web.http import Request from twisted.web import resource +from twisted.internet import reactor, endpoints +from autobahn.twisted.websocket import WebSocketServerFactory import json, ast from datetime import datetime import yaml @@ -11,10 +13,11 @@ from os import path from google.protobuf.message import DecodeError from core import CoreConfig, Utils -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 +from .config import PokkenConfig +from .base import PokkenBase +from .const import PokkenConstants +from .proto import jackal_pb2 +from .services import PokkenStunProtocol, PokkenAdmissionFactory, PokkenAdmissionProtocol class PokkenServlet(resource.Resource): @@ -91,7 +94,20 @@ class PokkenServlet(resource.Resource): def setup(self) -> None: # TODO: Setup stun, turn (UDP) and admission (WSS) servers - pass + reactor.listenUDP( + self.game_cfg.server.port_stun, PokkenStunProtocol(self.core_cfg, self.game_cfg, "Stun") + ) + + reactor.listenUDP( + self.game_cfg.server.port_turn, PokkenStunProtocol(self.core_cfg, self.game_cfg, "Turn") + ) + + factory = WebSocketServerFactory(f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}") + factory.protocol = PokkenAdmissionProtocol + + reactor.listenTCP( + self.game_cfg.server.port_admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg) + ) def render_POST( self, request: Request, version: int = 0, endpoints: str = "" diff --git a/titles/pokken/services.py b/titles/pokken/services.py new file mode 100644 index 0000000..ac12b72 --- /dev/null +++ b/titles/pokken/services.py @@ -0,0 +1,72 @@ +from twisted.internet.interfaces import IAddress +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.protocol import Protocol +from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory +from datetime import datetime +import logging +import json + +from core.config import CoreConfig +from .config import PokkenConfig +from .base import PokkenBase + +class PokkenStunProtocol(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig, type: str) -> None: + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("pokken") + self.server_type = type + + def datagramReceived(self, data, addr): + self.logger.debug( + f"{self.server_type} from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" + ) + self.transport.write(data, addr) + +# 474554202f20485454502f312e310d0a436f6e6e656374696f6e3a20557067726164650d0a486f73743a207469746c65732e6861793174732e6d653a393030330d0a5365632d576562536f636b65742d4b65793a204f4a6b6d522f376b646d6953326573483548783776413d3d0d0a5365632d576562536f636b65742d56657273696f6e3a2031330d0a557067726164653a20776562736f636b65740d0a557365722d4167656e743a20576562536f636b65742b2b2f302e332e300d0a0d0a +class PokkenAdmissionProtocol(WebSocketServerProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig): + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("pokken") + + self.base = PokkenBase(cfg, game_cfg) + + def onMessage(self, payload, isBinary: bool) -> None: + msg = json.loads(payload) + self.logger.debug(f"WebSocket from from {self.transport.getPeer().host}:{self.transport.getPeer().port} -> {self.transport.getHost().port} - {msg}") + + handler = getattr(self.base, f"handle_admission_{msg['api'].lower()}") + resp = handler(msg, self.transport.getPeer().host) + + if "type" not in resp: + resp['type'] = "res" + if "data" not in resp: + resp['data'] = {} + if "api" not in resp: + resp['api'] = msg["api"] + if "result" not in resp: + resp['result'] = 'true' + + self.logger.debug(f"Websocket response: {resp}") + self.sendMessage(json.dumps(resp).encode(), isBinary) + +# 0001002c2112a442334a0506a62efa71477dcd698022002872655455524e2053796e6320436c69656e7420302e33202d20524643353338392f7475726e2d3132 +class PokkenAdmissionFactory(WebSocketServerFactory): + protocol = PokkenAdmissionProtocol + + def __init__( + self, + cfg: CoreConfig, + game_cfg: PokkenConfig + ) -> None: + self.core_config = cfg + self.game_config = game_cfg + super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.server.port_admission}") + + def buildProtocol(self, addr: IAddress) -> Protocol: + p = self.protocol(self.core_config, self.game_config) + p.factory = self + return p From 1d10e798a5e1eb78a4ba53c7e06b3c4be0f097d6 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 12:29:28 -0400 Subject: [PATCH 255/589] Fixed few issues for SAO & removed static hex ranges --- titles/sao/base.py | 105 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 098b9c3..b4a71a4 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -62,9 +62,26 @@ class SaoBase: def handle_c11e(self, request: Any) -> bytes: #common/get_auth_card_data + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "cabinet_type" / Int8ub, # cabinet_type is a byte + "auth_type" / Int8ub, # auth_type is a byte + "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id + "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string + "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no + "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string + "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code + "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string + "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id + "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string + ) + + req_data = req_struct.parse(req) + access_code = req_data.access_code #Check authentication - access_code = bytes.fromhex(request[188:268]).decode("utf-16le") user_id = self.core_data.card.get_user_id_from_card( access_code ) if not user_id: @@ -92,6 +109,14 @@ class SaoBase: if user_id and not profile_data: profile_id = self.game_data.profile.create_profile(user_id) + self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) + self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) + profile_data = self.game_data.profile.get_profile(user_id) resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) @@ -104,7 +129,28 @@ class SaoBase: def handle_c104(self, request: Any) -> bytes: #common/login - access_code = bytes.fromhex(request[228:308]).decode("utf-16le") + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "cabinet_type" / Int8ub, # cabinet_type is a byte + "auth_type" / Int8ub, # auth_type is a byte + "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id + "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string + "store_name_size" / Rebuild(Int32ub, len_(this.store_name) * 2), # calculates the length of the store_name + "store_name" / PaddedString(this.store_name_size, "utf_16_le"), # store_name is a (zero) padded string + "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no + "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string + "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code + "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string + "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id + "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string + "free_ticket_distribution_target_flag" / Int8ub, # free_ticket_distribution_target_flag is a byte + ) + + req_data = req_struct.parse(req) + access_code = req_data.access_code + user_id = self.core_data.card.get_user_id_from_card( access_code ) profile_data = self.game_data.profile.get_profile(user_id) @@ -123,7 +169,17 @@ class SaoBase: def handle_c500(self, request: Any) -> bytes: #user_info/get_user_basic_data - user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + profile_data = self.game_data.profile.get_profile(user_id) resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) @@ -132,6 +188,7 @@ class SaoBase: def handle_c600(self, request: Any) -> bytes: #have_object/get_hero_log_user_data_list req = bytes.fromhex(request)[24:] + req_struct = Struct( Padding(16), "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id @@ -149,6 +206,7 @@ class SaoBase: def handle_c602(self, request: Any) -> bytes: #have_object/get_equipment_user_data_list req = bytes.fromhex(request)[24:] + req_struct = Struct( Padding(16), "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id @@ -165,8 +223,8 @@ class SaoBase: def handle_c604(self, request: Any) -> bytes: #have_object/get_item_user_data_list - #itemIdsData = self.game_data.static.get_item_ids(0, True) req = bytes.fromhex(request)[24:] + req_struct = Struct( Padding(16), "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id @@ -179,7 +237,6 @@ class SaoBase: item_data = self.game_data.item.get_user_items(user_id) resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data) - #resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() def handle_c606(self, request: Any) -> bytes: @@ -198,7 +255,17 @@ class SaoBase: def handle_c608(self, request: Any) -> bytes: #have_object/get_episode_append_data_list - user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + profile_data = self.game_data.profile.get_profile(user_id) resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) @@ -206,8 +273,8 @@ class SaoBase: def handle_c804(self, request: Any) -> bytes: #custom/get_party_data_list - req = bytes.fromhex(request)[24:] + req_struct = Struct( Padding(16), "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id @@ -479,7 +546,6 @@ class SaoBase: def handle_c904(self, request: Any) -> bytes: #quest/episode_play_start - req = bytes.fromhex(request)[24:] req_struct = Struct( @@ -718,10 +784,25 @@ class SaoBase: resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - def handle_c914(self, request: Any) -> bytes: + def handle_c914(self, request: Any) -> bytes: # TBD #quest/trial_tower_play_start - user_id = bytes.fromhex(request[100:124]).decode("utf-16le") - floor_id = int(request[130:132], 16) # not required but nice to know + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id + "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "trial_tower_id" / Int32ub, # trial_tower_id is an int + "play_mode" / Int8ub, # play_mode is a byte + + ) + + req_data = req_struct.parse(req) + + user_id = req_data.user_id + floor_id = req_data.trial_tower_id profile_data = self.game_data.profile.get_profile(user_id) resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) @@ -931,4 +1012,4 @@ class SaoBase: def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) - return resp.make() + return resp.make() \ No newline at end of file From 858b101a36102702afcf29d405d39a0bd5a770fe Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 24 Jun 2023 13:14:40 -0400 Subject: [PATCH 256/589] dbutils: add command to show versions --- core/data/database.py | 5 +++++ dbutils.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/core/data/database.py b/core/data/database.py index 719d05e..ffbefc0 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -333,3 +333,8 @@ class Data: if not failed: self.base.set_schema_ver(latest_ver, game) + + def show_versions(self) -> None: + all_game_versions = self.base.get_all_schema_vers() + for ver in all_game_versions: + self.logger.info(f"{ver['game']} -> v{ver['version']}") diff --git a/dbutils.py b/dbutils.py index d959232..caae9d8 100644 --- a/dbutils.py +++ b/dbutils.py @@ -84,5 +84,8 @@ if __name__ == "__main__": elif args.action == "cleanup": data.delete_hanging_users() + + elif args.action == "version": + data.show_versions() data.logger.info("Done") From 402e7534692cd534445cbcffeddcb1c8002194f5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 24 Jun 2023 15:09:38 -0400 Subject: [PATCH 257/589] wacca: fix tabbing error in util_put_items --- titles/wacca/base.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index eeafe4c..cca6aa5 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -1074,17 +1074,17 @@ class WaccaBase: 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, - ) + 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 From d5bff0e8918d6ea95aa023d931f59cf9df4bb88e Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 18:48:48 -0400 Subject: [PATCH 258/589] Stage progression done for SAO --- docs/game_specific_info.md | 2 +- titles/sao/base.py | 63 +++++++++++++++++++++------- titles/sao/handlers/base.py | 59 ++++++++++++++++---------- titles/sao/schema/item.py | 83 +++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 39 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 15efe7c..52b33a0 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -437,7 +437,7 @@ python dbutils.py --game SDEW upgrade ``` ### Notes -- Stages are currently force unlocked, no progression +- Tower Quests currently force unlocked, no progression - Co-Op (matching) is not supported - Shop is not functionnal - Player title is currently static and cannot be changed in-game diff --git a/titles/sao/base.py b/titles/sao/base.py index b4a71a4..7019dca 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -101,6 +101,10 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) + self.game_data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) + + # Force the tutorial stage to be completed due to potential crash in-game + self.logger.info(f"User Authenticated: { access_code } | { user_id }") @@ -116,6 +120,10 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) + self.game_data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) + + # Force the tutorial stage to be completed due to potential crash in-game + profile_data = self.game_data.profile.get_profile(user_id) @@ -304,8 +312,20 @@ class SaoBase: def handle_c900(self, request: Any) -> bytes: #quest/get_quest_scene_user_data_list // QuestScene.csv - questIdsData = self.game_data.static.get_quests_ids(0, True) - resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, questIdsData) + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + quest_data = self.game_data.item.get_quest_logs(user_id) + + resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, quest_data) return resp.make() def handle_c400(self, request: Any) -> bytes: @@ -678,8 +698,21 @@ class SaoBase: req_data = req_struct.parse(req) + # Add stage progression to database + user_id = req_data.user_id + episode_id = req_data.episode_id + quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) + clear_time = req_data.score_data[0].clear_time + combo_num = req_data.score_data[0].combo_num + total_damage = req_data.score_data[0].total_damage + concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + + if quest_clear_flag is True: + # Save stage progression - to be revised to avoid saving worse score + self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) + # Update the profile - profile = self.game_data.profile.get_profile(req_data.user_id) + profile = self.game_data.profile.get_profile(user_id) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) @@ -703,7 +736,7 @@ class SaoBase: # Update profile updated_profile = self.game_data.profile.put_profile( - req_data.user_id, + user_id, profile["user_type"], profile["nick_name"], player_level, @@ -715,8 +748,8 @@ class SaoBase: ) # Update heroes from the used party - play_session = self.game_data.item.get_session(req_data.user_id) - session_party = self.game_data.item.get_hero_party(req_data.user_id, play_session["user_party_team_id"]) + play_session = self.game_data.item.get_session(user_id) + session_party = self.game_data.item.get_hero_party(user_id, play_session["user_party_team_id"]) hero_list = [] hero_list.append(session_party["user_hero_log_id_1"]) @@ -724,12 +757,12 @@ class SaoBase: hero_list.append(session_party["user_hero_log_id_3"]) for i in range(0,len(hero_list)): - hero_data = self.game_data.item.get_hero_log(req_data.user_id, hero_list[i]) + hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) self.game_data.item.put_hero_log( - req_data.user_id, + user_id, hero_data["user_hero_log_id"], hero_data["log_level"], log_exp, @@ -752,11 +785,11 @@ class SaoBase: itemList = self.game_data.static.get_item_id(commonRewardId) if heroList: - self.game_data.item.put_hero_log(req_data.user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + self.game_data.item.put_hero_log(user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: - self.game_data.item.put_equipment_data(req_data.user_id, commonRewardId, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, commonRewardId, 1, 200, 0, 0, 0) if itemList: - self.game_data.item.put_item(req_data.user_id, commonRewardId) + self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): @@ -773,11 +806,11 @@ class SaoBase: equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) if heroList: - self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: - self.game_data.item.put_equipment_data(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: - self.game_data.item.put_item(req_data.user_id, randomized_unanalyzed_id['CommonRewardId']) + self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) # Send response @@ -1012,4 +1045,4 @@ class SaoBase: def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) - return resp.make() \ No newline at end of file + return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index a9d336a..eea9850 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1707,24 +1707,43 @@ class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, questIdsData) -> None: + def __init__(self, cmd, quest_data) -> None: super().__init__(cmd) self.result = 1 # quest_scene_user_data_list_size - self.quest_type = [1] * len(questIdsData) - self.quest_scene_id = questIdsData - self.clear_flag = [1] * len(questIdsData) + self.quest_type = [] + self.quest_scene_id = [] + self.clear_flag = [] # quest_scene_best_score_user_data - self.clear_time = 300 - self.combo_num = 0 - self.total_damage = "0" - self.concurrent_destroying_num = 1 + self.clear_time = [] + self.combo_num = [] + self.total_damage = [] #string + self.concurrent_destroying_num = [] + + for i in range(len(quest_data)): + self.quest_type.append(1) + self.quest_scene_id.append(quest_data[i][2]) + self.clear_flag.append(int(quest_data[i][3])) + + self.clear_time.append(quest_data[i][4]) + self.combo_num.append(quest_data[i][5]) + self.total_damage.append(0) #totally absurd but Int16ul[1] is a big problem due to different lenghts... + self.concurrent_destroying_num.append(quest_data[i][7]) # quest_scene_ex_bonus_user_data_list self.achievement_flag = [[1, 1, 1],[1, 1, 1]] self.ex_bonus_table_id = [[1, 2, 3],[4, 5, 6]] + + + self.quest_type = list(map(int,self.quest_type)) #int + self.quest_scene_id = list(map(int,self.quest_scene_id)) #int + self.clear_flag = list(map(int,self.clear_flag)) #int + self.clear_time = list(map(int,self.clear_time)) #int + self.combo_num = list(map(int,self.combo_num)) #int + self.total_damage = list(map(str,self.total_damage)) #string + self.concurrent_destroying_num = list(map(int,self.combo_num)) #int def make(self) -> bytes: #new stuff @@ -1737,7 +1756,7 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): "clear_time" / Int32ub, # big endian "combo_num" / Int32ub, # big endian "total_damage_size" / Int32ub, # big endian - "total_damage" / Int16ul[len(self.total_damage)], + "total_damage" / Int16ul[1], "concurrent_destroying_num" / Int16ub, ) @@ -1765,7 +1784,7 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): ))) for i in range(len(self.quest_scene_id)): - quest_data = dict( + quest_resp_data = dict( quest_type=self.quest_type[i], quest_scene_id=self.quest_scene_id[i], clear_flag=self.clear_flag[i], @@ -1776,22 +1795,16 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): quest_scene_ex_bonus_user_data_list=[], ) - quest_data["quest_scene_best_score_user_data"].append(dict( - clear_time=self.clear_time, - combo_num=self.combo_num, - total_damage_size=len(self.total_damage) * 2, - total_damage=[ord(x) for x in self.total_damage], - concurrent_destroying_num=self.concurrent_destroying_num, + quest_resp_data["quest_scene_best_score_user_data"].append(dict( + clear_time=self.clear_time[i], + combo_num=self.combo_num[i], + total_damage_size=len(self.total_damage[i]) * 2, + total_damage=[ord(x) for x in self.total_damage[i]], + concurrent_destroying_num=self.concurrent_destroying_num[i], )) - ''' - quest_data["quest_scene_ex_bonus_user_data_list"].append(dict( - ex_bonus_table_id=self.ex_bonus_table_id[i], - achievement_flag=self.achievement_flag[i], - )) - ''' - resp_data.quest_scene_user_data_list.append(quest_data) + resp_data.quest_scene_user_data_list.append(quest_resp_data) # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 858267e..8b46723 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -84,6 +84,26 @@ hero_party = Table( mysql_charset="utf8mb4", ) +quest = Table( + "sao_player_quest", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("episode_id", Integer, nullable=False), + Column("quest_clear_flag", Boolean, nullable=False), + Column("clear_time", Integer, nullable=False), + Column("combo_num", Integer, nullable=False), + Column("total_damage", Integer, nullable=False), + Column("concurrent_destroying_num", Integer, nullable=False), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "episode_id", name="sao_player_quest_uk"), + mysql_charset="utf8mb4", +) + sessions = Table( "sao_play_sessions", metadata, @@ -227,6 +247,34 @@ class SaoItemData(BaseData): return result.lastrowid + def put_player_quest(self, user_id: int, episode_id: int, quest_clear_flag: bool, clear_time: int, combo_num: int, total_damage: int, concurrent_destroying_num: int) -> Optional[int]: + sql = insert(quest).values( + user=user_id, + episode_id=episode_id, + quest_clear_flag=quest_clear_flag, + clear_time=clear_time, + combo_num=combo_num, + total_damage=total_damage, + concurrent_destroying_num=concurrent_destroying_num + ) + + conflict = sql.on_duplicate_key_update( + quest_clear_flag=quest_clear_flag, + clear_time=clear_time, + combo_num=combo_num, + total_damage=total_damage, + concurrent_destroying_num=concurrent_destroying_num + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert quest! user: {user_id}, episode_id: {episode_id}" + ) + return None + + return result.lastrowid + def get_user_equipment(self, user_id: int, equipment_id: int) -> Optional[Dict]: sql = equipment_data.select(equipment_data.c.user == user_id and equipment_data.c.equipment_id == equipment_id) @@ -319,6 +367,41 @@ class SaoItemData(BaseData): return None return result.fetchone() + def get_quest_log( + self, user_id: int, episode_id: int = None + ) -> Optional[List[Row]]: + """ + A catch-all quest lookup given a profile and episode_id + """ + sql = quest.select( + and_( + quest.c.user == user_id, + quest.c.episode_id == episode_id if episode_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_quest_logs( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all quest lookup given a profile + """ + sql = quest.select( + and_( + quest.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_session( self, user_id: int = None ) -> Optional[List[Row]]: From 391edd3354e420c08f5e643e64779c556c9af81f Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 20:09:37 -0400 Subject: [PATCH 259/589] Tower progression now working for SAO --- docs/game_specific_info.md | 1 - titles/sao/base.py | 51 ++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 52b33a0..d2ae6d8 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -437,7 +437,6 @@ python dbutils.py --game SDEW upgrade ``` ### Notes -- Tower Quests currently force unlocked, no progression - Co-Op (matching) is not supported - Shop is not functionnal - Player title is currently static and cannot be changed in-game diff --git a/titles/sao/base.py b/titles/sao/base.py index 7019dca..d1750c6 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -817,7 +817,7 @@ class SaoBase: resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - def handle_c914(self, request: Any) -> bytes: # TBD + def handle_c914(self, request: Any) -> bytes: #quest/trial_tower_play_start req = bytes.fromhex(request)[24:] @@ -932,8 +932,33 @@ class SaoBase: req_data = req_struct.parse(req) + # Add tower progression to database + user_id = req_data.user_id + trial_tower_id = req_data.trial_tower_id + quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) + clear_time = req_data.score_data[0].clear_time + combo_num = req_data.score_data[0].combo_num + total_damage = req_data.score_data[0].total_damage + concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + + if quest_clear_flag is True: + # Save tower progression - to be revised to avoid saving worse score + if trial_tower_id == 10: + trial_tower_id = 10001 + elif trial_tower_id == 20: + trial_tower_id = 10002 + elif trial_tower_id == 30: + trial_tower_id = 10003 + elif trial_tower_id == 40: + trial_tower_id = 10004 + elif trial_tower_id == 50: + trial_tower_id = 10005 + else: + trial_tower_id = trial_tower_id + 3000 + self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) + # Update the profile - profile = self.game_data.profile.get_profile(req_data.user_id) + profile = self.game_data.profile.get_profile(user_id) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) @@ -956,7 +981,7 @@ class SaoBase: break updated_profile = self.game_data.profile.put_profile( - req_data.user_id, + user_id, profile["user_type"], profile["nick_name"], player_level, @@ -968,8 +993,8 @@ class SaoBase: ) # Update heroes from the used party - play_session = self.game_data.item.get_session(req_data.user_id) - session_party = self.game_data.item.get_hero_party(req_data.user_id, play_session["user_party_team_id"]) + play_session = self.game_data.item.get_session(user_id) + session_party = self.game_data.item.get_hero_party(user_id, play_session["user_party_team_id"]) hero_list = [] hero_list.append(session_party["user_hero_log_id_1"]) @@ -977,12 +1002,12 @@ class SaoBase: hero_list.append(session_party["user_hero_log_id_3"]) for i in range(0,len(hero_list)): - hero_data = self.game_data.item.get_hero_log(req_data.user_id, hero_list[i]) + hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) self.game_data.item.put_hero_log( - req_data.user_id, + user_id, hero_data["user_hero_log_id"], hero_data["log_level"], log_exp, @@ -1005,11 +1030,11 @@ class SaoBase: itemList = self.game_data.static.get_item_id(commonRewardId) if heroList: - self.game_data.item.put_hero_log(req_data.user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + self.game_data.item.put_hero_log(user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: - self.game_data.item.put_equipment_data(req_data.user_id, commonRewardId, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, commonRewardId, 1, 200, 0, 0, 0) if itemList: - self.game_data.item.put_item(req_data.user_id, commonRewardId) + self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): @@ -1026,11 +1051,11 @@ class SaoBase: equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) if heroList: - self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: - self.game_data.item.put_equipment_data(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: - self.game_data.item.put_item(req_data.user_id, randomized_unanalyzed_id['CommonRewardId']) + self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) # Send response From 01b5282899c07ed0628fe269ae670b19de7d0248 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 20:31:14 -0400 Subject: [PATCH 260/589] small fix about next tower stage progression for SAO --- titles/sao/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index d1750c6..2ae672d 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -935,6 +935,7 @@ class SaoBase: # Add tower progression to database user_id = req_data.user_id trial_tower_id = req_data.trial_tower_id + next_tower_id = 0 quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) clear_time = req_data.score_data[0].clear_time combo_num = req_data.score_data[0].combo_num @@ -945,17 +946,23 @@ class SaoBase: # Save tower progression - to be revised to avoid saving worse score if trial_tower_id == 10: trial_tower_id = 10001 + next_tower_id = 3011 elif trial_tower_id == 20: trial_tower_id = 10002 + next_tower_id = 3021 elif trial_tower_id == 30: trial_tower_id = 10003 + next_tower_id = 3031 elif trial_tower_id == 40: trial_tower_id = 10004 + next_tower_id = 3041 elif trial_tower_id == 50: trial_tower_id = 10005 + next_tower_id = 3051 else: trial_tower_id = trial_tower_id + 3000 - self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) + self.game_data.item.put_player_quest(user_id, trial_tower_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) + self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) # Update the profile profile = self.game_data.profile.get_profile(user_id) From 571b92d0cdd14c8eb5bd3a18b4005aab5d23b69e Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 20:33:30 -0400 Subject: [PATCH 261/589] forgot one line, see previous commit --- titles/sao/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 2ae672d..49e8f9b 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -961,6 +961,8 @@ class SaoBase: next_tower_id = 3051 else: trial_tower_id = trial_tower_id + 3000 + next_tower_id = trial_tower_id + 1 + self.game_data.item.put_player_quest(user_id, trial_tower_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) From 08ebb5c90706979c6d7c3afa9d1ac7769b933e24 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 24 Jun 2023 20:42:00 -0400 Subject: [PATCH 262/589] another quick fix for SAO tower stage --- titles/sao/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 49e8f9b..c2266e8 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -944,18 +944,28 @@ class SaoBase: if quest_clear_flag is True: # Save tower progression - to be revised to avoid saving worse score - if trial_tower_id == 10: + if trial_tower_id == 9: + next_tower_id = 10001 + elif trial_tower_id == 10: trial_tower_id = 10001 next_tower_id = 3011 + elif trial_tower_id == 19: + next_tower_id = 10002 elif trial_tower_id == 20: trial_tower_id = 10002 next_tower_id = 3021 + elif trial_tower_id == 29: + next_tower_id = 10003 elif trial_tower_id == 30: trial_tower_id = 10003 next_tower_id = 3031 + elif trial_tower_id == 39: + next_tower_id = 10004 elif trial_tower_id == 40: trial_tower_id = 10004 next_tower_id = 3041 + elif trial_tower_id == 49: + next_tower_id = 10005 elif trial_tower_id == 50: trial_tower_id = 10005 next_tower_id = 3051 From ec9ad1ebb0099aa735c5ab6243beb1803fb9310a Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 25 Jun 2023 00:08:50 -0400 Subject: [PATCH 263/589] fixing stage progression for SAO --- titles/sao/base.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index c2266e8..c2a855b 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -709,6 +709,15 @@ class SaoBase: if quest_clear_flag is True: # Save stage progression - to be revised to avoid saving worse score + + # Reference Episode.csv but Chapter 3,4 and 5 reports id 0 + if episode_id > 10000 and episode_id < 11000: + episode_id = episode_id - 9000 + elif episode_id > 20000 and episode_id < 21000: + episode_id = episode_id - 19000 + elif episode_id > 30000 and episode_id < 31000: + episode_id = episode_id - 29000 + self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) # Update the profile @@ -829,7 +838,17 @@ class SaoBase: "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string "trial_tower_id" / Int32ub, # trial_tower_id is an int "play_mode" / Int8ub, # play_mode is a byte - + Padding(3), + "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, + "play_start_request_data" / Array(this.play_start_request_data_length, Struct( + "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id + "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string + "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage + "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string + "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage + "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string + "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte + )), ) req_data = req_struct.parse(req) @@ -838,6 +857,14 @@ class SaoBase: floor_id = req_data.trial_tower_id profile_data = self.game_data.profile.get_profile(user_id) + self.game_data.item.create_session( + user_id, + int(req_data.play_start_request_data[0].user_party_id), + req_data.trial_tower_id, + req_data.play_mode, + req_data.play_start_request_data[0].quest_drop_boost_apply_flag + ) + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) return resp.make() @@ -972,9 +999,14 @@ class SaoBase: else: trial_tower_id = trial_tower_id + 3000 next_tower_id = trial_tower_id + 1 - + self.game_data.item.put_player_quest(user_id, trial_tower_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) - self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) + + # Check if next stage is already done + checkQuest = self.game_data.item.get_quest_log(user_id, next_tower_id) + if not checkQuest: + if next_tower_id != 3101: + self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) # Update the profile profile = self.game_data.profile.get_profile(user_id) From 514f786e2d21e549c3a386b2a3932c3a74226005 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 25 Jun 2023 01:09:23 -0400 Subject: [PATCH 264/589] pokken: Switch to using external STUN server --- example_config/pokken.yaml | 13 +++++--- titles/pokken/base.py | 33 +++++++++--------- titles/pokken/config.py | 68 ++++++++++++++++++++++++-------------- titles/pokken/index.py | 29 ++++++---------- titles/pokken/services.py | 40 ++++++++++------------ 5 files changed, 97 insertions(+), 86 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 225e980..423d9d3 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -2,8 +2,11 @@ server: hostname: "localhost" enable: True loglevel: "info" - port: 9000 - port_stun: 9001 - port_turn: 9002 - port_admission: 9003 - auto_register: True \ No newline at end of file + auto_register: True + enable_matching: False + stun_server_host: "stunserver.stunprotocol.org" + stun_server_port: 3478 + +ports: + game: 9000 + admission: 9001 diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 306532d..50bc760 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -44,19 +44,19 @@ class PokkenBase: biwa_setting = { "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", - "port": self.game_cfg.server.port, + "port": self.game_cfg.ports.game, "url": "/SDAK/100/matching", }, "StunServer": { - "addr": self.game_cfg.server.hostname, - "port": self.game_cfg.server.port_stun, + "addr": self.game_cfg.server.stun_server_host, + "port": self.game_cfg.server.stun_server_port, }, "TurnServer": { - "addr": self.game_cfg.server.hostname, - "port": self.game_cfg.server.port_turn, + "addr": self.game_cfg.server.stun_server_host, + "port": self.game_cfg.server.stun_server_port, }, - "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}", - "locationId": 123, + "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}", + "locationId": 123, # FIXME: Get arcade's ID from the database "logfilename": "JackalMatchingLibrary.log", "biwalogfilename": "./biwa.log", } @@ -94,6 +94,7 @@ class PokkenBase: res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS settings = jackal_pb2.LoadClientSettingsResponseData() + # TODO: Make configurable settings.money_magnification = 1 settings.continue_bonus_exp = 100 settings.continue_fight_money = 100 @@ -356,12 +357,11 @@ class PokkenBase: 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":[] + "sessionId":"12345678", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, """ return { "data": { @@ -379,10 +379,13 @@ class PokkenBase: ) -> Dict: return {} + def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + return {} + def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: self.logger.info(f"Admission: JoinSession from {req_ip}") return { 'data': { - "id": 123 + "id": 12345678 } - } \ No newline at end of file + } diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 84da8d2..d3741af 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -25,30 +25,6 @@ class PokkenServerConfig: ) ) - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port", default=9000 - ) - - @property - def port_stun(self) -> int: - return CoreConfig.get_config_field( - 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=9002 - ) - - @property - def port_admission(self) -> int: - return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port_admission", default=9003 - ) - @property def auto_register(self) -> bool: """ @@ -59,7 +35,51 @@ class PokkenServerConfig: self.__config, "pokken", "server", "auto_register", default=True ) + @property + def enable_matching(self) -> bool: + """ + If global matching should happen + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "enable_matching", default=False + ) + + @property + def stun_server_host(self) -> str: + """ + Hostname of the EXTERNAL stun server the game should connect to. This is not handled by artemis. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org" + ) + + @property + def stun_server_port(self) -> int: + """ + Port of the EXTERNAL stun server the game should connect to. This is not handled by artemis. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "stun_server_port", default=3478 + ) + +class PokkenPortsConfig: + def __init__(self, parent_config: "PokkenConfig"): + self.__config = parent_config + + @property + def game(self) -> int: + return CoreConfig.get_config_field( + self.__config, "pokken", "ports", "game", default=9000 + ) + + @property + def admission(self) -> int: + return CoreConfig.get_config_field( + self.__config, "pokken", "ports", "admission", default=9001 + ) + class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) + self.ports = PokkenPortsConfig(self) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 1140d41..3ccb3fc 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,8 +1,7 @@ from typing import Tuple from twisted.web.http import Request from twisted.web import resource -from twisted.internet import reactor, endpoints -from autobahn.twisted.websocket import WebSocketServerFactory +from twisted.internet import reactor import json, ast from datetime import datetime import yaml @@ -17,7 +16,7 @@ from .config import PokkenConfig from .base import PokkenBase from .const import PokkenConstants from .proto import jackal_pb2 -from .services import PokkenStunProtocol, PokkenAdmissionFactory, PokkenAdmissionProtocol +from .services import PokkenAdmissionFactory class PokkenServlet(resource.Resource): @@ -72,7 +71,7 @@ class PokkenServlet(resource.Resource): return ( True, - f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", + f"https://{game_cfg.server.hostname}:{game_cfg.ports.game}/{game_code}/$v/", f"{game_cfg.server.hostname}/SDAK/$v/", ) @@ -93,21 +92,10 @@ class PokkenServlet(resource.Resource): return (True, "PKF1") def setup(self) -> None: - # TODO: Setup stun, turn (UDP) and admission (WSS) servers - reactor.listenUDP( - self.game_cfg.server.port_stun, PokkenStunProtocol(self.core_cfg, self.game_cfg, "Stun") - ) - - reactor.listenUDP( - self.game_cfg.server.port_turn, PokkenStunProtocol(self.core_cfg, self.game_cfg, "Turn") - ) - - factory = WebSocketServerFactory(f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}") - factory.protocol = PokkenAdmissionProtocol - - reactor.listenTCP( - self.game_cfg.server.port_admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg) - ) + if self.game_cfg.server.enable_matching: + reactor.listenTCP( + self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg) + ) def render_POST( self, request: Request, version: int = 0, endpoints: str = "" @@ -144,6 +132,9 @@ class PokkenServlet(resource.Resource): return ret def handle_matching(self, request: Request) -> bytes: + if not self.game_cfg.server.enable_matching: + return b"" + content = request.content.getvalue() client_ip = Utils.get_ip_addr(request) diff --git a/titles/pokken/services.py b/titles/pokken/services.py index ac12b72..952c232 100644 --- a/titles/pokken/services.py +++ b/titles/pokken/services.py @@ -1,8 +1,8 @@ from twisted.internet.interfaces import IAddress -from twisted.internet.protocol import DatagramProtocol from twisted.internet.protocol import Protocol from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory -from datetime import datetime +from autobahn.websocket.types import ConnectionRequest +from typing import Dict import logging import json @@ -10,21 +10,6 @@ from core.config import CoreConfig from .config import PokkenConfig from .base import PokkenBase -class PokkenStunProtocol(DatagramProtocol): - def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig, type: str) -> None: - super().__init__() - self.core_config = cfg - self.game_config = game_cfg - self.logger = logging.getLogger("pokken") - self.server_type = type - - def datagramReceived(self, data, addr): - self.logger.debug( - f"{self.server_type} from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" - ) - self.transport.write(data, addr) - -# 474554202f20485454502f312e310d0a436f6e6e656374696f6e3a20557067726164650d0a486f73743a207469746c65732e6861793174732e6d653a393030330d0a5365632d576562536f636b65742d4b65793a204f4a6b6d522f376b646d6953326573483548783776413d3d0d0a5365632d576562536f636b65742d56657273696f6e3a2031330d0a557067726164653a20776562736f636b65740d0a557365722d4167656e743a20576562536f636b65742b2b2f302e332e300d0a0d0a class PokkenAdmissionProtocol(WebSocketServerProtocol): def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig): super().__init__() @@ -33,27 +18,36 @@ class PokkenAdmissionProtocol(WebSocketServerProtocol): self.logger = logging.getLogger("pokken") self.base = PokkenBase(cfg, game_cfg) + + def onConnect(self, request: ConnectionRequest) -> None: + self.logger.debug(f"Admission: Connection from {request.peer}") + + def onClose(self, wasClean: bool, code: int, reason: str) -> None: + self.logger.debug(f"Admission: Connection with {self.transport.getPeer().host} closed {'cleanly ' if wasClean else ''}with code {code} - {reason}") def onMessage(self, payload, isBinary: bool) -> None: - msg = json.loads(payload) - self.logger.debug(f"WebSocket from from {self.transport.getPeer().host}:{self.transport.getPeer().port} -> {self.transport.getHost().port} - {msg}") + msg: Dict = json.loads(payload) + self.logger.debug(f"Admission: Message from {self.transport.getPeer().host}:{self.transport.getPeer().port} - {msg}") - handler = getattr(self.base, f"handle_admission_{msg['api'].lower()}") + api = msg.get("api", "noop") + handler = getattr(self.base, f"handle_admission_{api.lower()}") resp = handler(msg, self.transport.getPeer().host) + + if resp is None: + resp = {} if "type" not in resp: resp['type'] = "res" if "data" not in resp: resp['data'] = {} if "api" not in resp: - resp['api'] = msg["api"] + resp['api'] = api if "result" not in resp: resp['result'] = 'true' self.logger.debug(f"Websocket response: {resp}") self.sendMessage(json.dumps(resp).encode(), isBinary) -# 0001002c2112a442334a0506a62efa71477dcd698022002872655455524e2053796e6320436c69656e7420302e33202d20524643353338392f7475726e2d3132 class PokkenAdmissionFactory(WebSocketServerFactory): protocol = PokkenAdmissionProtocol @@ -64,7 +58,7 @@ class PokkenAdmissionFactory(WebSocketServerFactory): ) -> None: self.core_config = cfg self.game_config = game_cfg - super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.server.port_admission}") + super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.ports.admission}") def buildProtocol(self, addr: IAddress) -> Protocol: p = self.protocol(self.core_config, self.game_config) From aae4afe7b885cc585f031e42500d9d772f616353 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 25 Jun 2023 11:59:17 -0400 Subject: [PATCH 265/589] Adding debug logging to SAO --- titles/sao/index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index b74e500..69a3db5 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -97,20 +97,20 @@ class SaoServlet(resource.Resource): req_url = request.uri.decode() if req_url == "/matching": self.logger.info("Matching request") - + request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") sao_request = request.content.getvalue().hex() - #sao_request = sao_request[:32] handler = getattr(self.base, f"handle_{sao_request[:4]}", None) if handler is None: self.logger.info(f"Generic Handler for {req_url} - {sao_request[:4]}") - #self.logger.debug(f"Request: {request.content.getvalue().hex()}") + self.logger.debug(f"Request: {request.content.getvalue().hex()}") resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1) self.logger.debug(f"Response: {resp.make().hex()}") return resp.make() self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") + self.logger.debug(f"Response: {handler(sao_request).hex()}") return handler(sao_request) \ No newline at end of file From 17508f09b2f8eee67a4c7d6576515813aa349705 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 25 Jun 2023 13:47:31 -0400 Subject: [PATCH 266/589] fixed episode VP saving & hero level in DB for SAO --- titles/sao/base.py | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index c2a855b..a519401 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -707,6 +707,11 @@ class SaoBase: total_damage = req_data.score_data[0].total_damage concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + profile = self.game_data.profile.get_profile(user_id) + vp = int(profile["own_vp"]) + exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason + col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + if quest_clear_flag is True: # Save stage progression - to be revised to avoid saving worse score @@ -720,11 +725,8 @@ class SaoBase: self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) - # Update the profile - profile = self.game_data.profile.get_profile(user_id) - - exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + vp = int(profile["own_vp"]) + 10 #always 10 VP per cleared stage + # Calculate level based off experience and the CSV list with open(r'titles/sao/data/PlayerRank.csv') as csv_file: @@ -751,7 +753,7 @@ class SaoBase: player_level, exp, col, - profile["own_vp"], + vp, profile["own_yui_medal"], profile["setting_title_id"] ) @@ -770,10 +772,27 @@ class SaoBase: log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + # Calculate hero level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for e in range(0,len(data)): + if log_exp>=int(data[e][1]) and log_exp=int(data[e][1]) and log_exp Date: Sun, 25 Jun 2023 14:40:34 -0400 Subject: [PATCH 267/589] fixing hero party saving for SAO --- titles/sao/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index a519401..ece4654 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -537,6 +537,7 @@ class SaoBase: req_data = req_struct.parse(req) user_id = req_data.user_id + party_hero_list = [] for party_team in req_data.party_data_list[0].party_team_data_list: hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) @@ -561,6 +562,10 @@ class SaoBase: party_team["skill_slot5_skill_id"] ) + party_hero_list.append(party_team["user_hero_log_id"]) + + self.game_data.item.put_hero_party(user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) + resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() From 0c6d9a36cefa644ad00923f5d281f24f0ea312f8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 25 Jun 2023 18:43:00 -0400 Subject: [PATCH 268/589] mai2: add movie server endpoints --- titles/mai2/base.py | 2 +- titles/mai2/index.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5a8b0c6..e77b2e1 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -36,7 +36,7 @@ class Mai2Base: "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie/", + "movieServerUri": self.old_server + "movie", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", diff --git a/titles/mai2/index.py b/titles/mai2/index.py index c4d6874..1d9b8bf 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -112,6 +112,10 @@ class Mai2Servlet: def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') + + elif url_path.startswith("movie/"): + self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}") + return b"" req_raw = request.content.getvalue() url = request.uri.decode() @@ -211,6 +215,10 @@ class Mai2Servlet: self.logger.info(f"v{version} GET {url_path}") url_split = url_path.split("/") + if url_split[0] == "movie": + if url_split[1] == "moviestart": + return json.dumps({"moviestart":{"status":"OK"}}).encode() + if url_split[0] == "old": if url_split[1] == "ping": self.logger.info(f"v{version} old server ping") From e3d38dacde7d7d6e82e625496803ee4e860e5f9c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 25 Jun 2023 19:10:34 -0400 Subject: [PATCH 269/589] mai2: fix movies --- titles/mai2/base.py | 2 +- titles/mai2/config.py | 29 +++++++++++++++++++++++++++++ titles/mai2/index.py | 6 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index e77b2e1..ef15abe 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -36,7 +36,7 @@ class Mai2Base: "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie", + "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", diff --git a/titles/mai2/config.py b/titles/mai2/config.py index d5ed41f..d63c0b2 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -41,8 +41,37 @@ class Mai2DeliverConfig: self.__config, "mai2", "server", "content_folder", default="" ) +class Mai2UploadsConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def photos(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos", default=False + ) + + @property + def photos_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos_dir", default="" + ) + + @property + def movies(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies", default=False + ) + + @property + def movies_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies_dir", default="" + ) + class Mai2Config(dict): def __init__(self) -> None: self.server = Mai2ServerConfig(self) self.deliver = Mai2DeliverConfig(self) + self.uploads = Mai2UploadsConfig(self) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 1d9b8bf..c250894 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -113,7 +113,7 @@ class Mai2Servlet: if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') - elif url_path.startswith("movie/"): + elif url_path.startswith("api/movie/"): self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}") return b"" @@ -215,8 +215,8 @@ class Mai2Servlet: self.logger.info(f"v{version} GET {url_path}") url_split = url_path.split("/") - if url_split[0] == "movie": - if url_split[1] == "moviestart": + if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie": + if url_split[2] == "moviestart": return json.dumps({"moviestart":{"status":"OK"}}).encode() if url_split[0] == "old": From 5155353360e00e752b21c019d356d4f00bc92d03 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 26 Jun 2023 19:30:03 -0400 Subject: [PATCH 270/589] fixing chapter progression after chapter 2 on SAO --- titles/sao/base.py | 16 +++++++++++----- titles/sao/schema/static.py | 8 ++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index ece4654..58bc3df 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -720,13 +720,19 @@ class SaoBase: if quest_clear_flag is True: # Save stage progression - to be revised to avoid saving worse score - # Reference Episode.csv but Chapter 3,4 and 5 reports id 0 + # Reference Episode.csv but Chapter 2,3,4 and 5 reports id -1, match using /10 + last digits if episode_id > 10000 and episode_id < 11000: + # Starts at 1001 episode_id = episode_id - 9000 - elif episode_id > 20000 and episode_id < 21000: - episode_id = episode_id - 19000 - elif episode_id > 30000 and episode_id < 31000: - episode_id = episode_id - 29000 + elif episode_id > 20000: + # Starts at 2001 + stage_id = str(episode_id)[-2:] + episode_id = episode_id / 10 + episode_id = int(episode_id) + int(stage_id) + + # Match episode_id with the questSceneId saved in the DB through sortNo + questId = self.game_data.static.get_quests_id(episode_id) + episode_id = questId[2] self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index 670e3b2..ce9a6a9 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -267,6 +267,14 @@ class SaoStaticData(BaseData): return None return result.lastrowid + def get_quests_id(self, sortNo: int) -> Optional[Dict]: + sql = quest.select(quest.c.sortNo == sortNo) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by( quest.c.questSceneId.asc() From 127e6f8aa8632f9cae05dee1de9559a7da36c5c4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 27 Jun 2023 00:32:35 -0400 Subject: [PATCH 271/589] mai2: add finale databases --- core/data/schema/versions/SDEZ_5_rollback.sql | 78 +++++++++ core/data/schema/versions/SDEZ_6_upgrade.sql | 62 +++++++ titles/mai2/__init__.py | 2 +- titles/mai2/schema/item.py | 79 ++++----- titles/mai2/schema/profile.py | 155 ++++++++++++++++++ titles/mai2/schema/score.py | 96 +++++++++++ 6 files changed, 432 insertions(+), 40 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_5_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_6_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_5_rollback.sql b/core/data/schema/versions/SDEZ_5_rollback.sql new file mode 100644 index 0000000..1507faa --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_rollback.sql @@ -0,0 +1,78 @@ +DELETE FROM mai2_static_event WHERE version < 13; +UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_music WHERE version < 13; +UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_ticket WHERE version < 13; +UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_cards WHERE version < 13; +UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_detail WHERE version < 13; +UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_extend WHERE version < 13; +UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_option WHERE version < 13; +UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_ghost WHERE version < 13; +UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_rating WHERE version < 13; +UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13; + +DROP TABLE maimai_score_best; +DROP TABLE maimai_playlog; +DROP TABLE maimai_profile_detail; +DROP TABLE maimai_profile_option; +DROP TABLE maimai_profile_web_option; +DROP TABLE maimai_profile_grade_status; + +ALTER TABLE mai2_item_character DROP COLUMN point; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NOT NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NOT NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NOT NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NOT NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_6_upgrade.sql b/core/data/schema/versions/SDEZ_6_upgrade.sql new file mode 100644 index 0000000..06b2d45 --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_upgrade.sql @@ -0,0 +1,62 @@ +UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000; + +ALTER TABLE mai2_item_character ADD point int(11) NULL; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; \ No newline at end of file diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index dc8fc84..7063ee6 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -16,4 +16,4 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, ] -current_schema_version = 5 +current_schema_version = 6 diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 6b70ed1..4e20383 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -18,10 +18,11 @@ character = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("characterId", Integer, nullable=False), - Column("level", Integer, nullable=False, server_default="1"), - Column("awakening", Integer, nullable=False, server_default="0"), - Column("useCount", Integer, nullable=False, server_default="0"), + Column("characterId", Integer), + Column("level", Integer), + Column("awakening", Integer), + Column("useCount", Integer), + Column("point", Integer), UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), mysql_charset="utf8mb4", ) @@ -35,12 +36,12 @@ card = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("cardId", Integer, nullable=False), - Column("cardTypeId", Integer, nullable=False), - Column("charaId", Integer, nullable=False), - Column("mapId", Integer, nullable=False), - Column("startDate", TIMESTAMP, nullable=False, server_default=func.now()), - Column("endDate", TIMESTAMP, nullable=False), + Column("cardId", Integer), + Column("cardTypeId", Integer), + Column("charaId", Integer), + Column("mapId", Integer), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP), UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), mysql_charset="utf8mb4", ) @@ -54,10 +55,10 @@ item = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("itemId", Integer, nullable=False), - Column("itemKind", Integer, nullable=False), - Column("stock", Integer, nullable=False, server_default="1"), - Column("isValid", Boolean, nullable=False, server_default="1"), + Column("itemId", Integer), + Column("itemKind", Integer), + Column("stock", Integer), + Column("isValid", Boolean), UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), mysql_charset="utf8mb4", ) @@ -71,11 +72,11 @@ map = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("mapId", Integer, nullable=False), - Column("distance", Integer, nullable=False), - Column("isLock", Boolean, nullable=False, server_default="0"), - Column("isClear", Boolean, nullable=False, server_default="0"), - Column("isComplete", Boolean, nullable=False, server_default="0"), + Column("mapId", Integer), + Column("distance", Integer), + Column("isLock", Boolean), + Column("isClear", Boolean), + Column("isComplete", Boolean), UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), mysql_charset="utf8mb4", ) @@ -89,10 +90,10 @@ login_bonus = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("bonusId", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("isCurrent", Boolean, nullable=False, server_default="0"), - Column("isComplete", Boolean, nullable=False, server_default="0"), + Column("bonusId", Integer), + Column("point", Integer), + Column("isCurrent", Boolean), + Column("isComplete", Boolean), UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), mysql_charset="utf8mb4", ) @@ -106,12 +107,12 @@ friend_season_ranking = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("seasonId", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("rank", Integer, nullable=False), - Column("rewardGet", Boolean, nullable=False), - Column("userName", String(8), nullable=False), - Column("recordDate", TIMESTAMP, nullable=False), + Column("seasonId", Integer), + Column("point", Integer), + Column("rank", Integer), + Column("rewardGet", Boolean), + Column("userName", String(8)), + Column("recordDate", TIMESTAMP), UniqueConstraint( "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" ), @@ -127,7 +128,7 @@ favorite = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("itemKind", Integer, nullable=False), + Column("itemKind", Integer), Column("itemIdList", JSON), UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"), mysql_charset="utf8mb4", @@ -142,10 +143,10 @@ charge = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("chargeId", Integer, nullable=False), - Column("stock", Integer, nullable=False), - Column("purchaseDate", String(255), nullable=False), - Column("validDate", String(255), nullable=False), + Column("chargeId", Integer), + Column("stock", Integer), + Column("purchaseDate", String(255)), + Column("validDate", String(255)), UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), mysql_charset="utf8mb4", ) @@ -161,11 +162,11 @@ print_detail = Table( ), 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("printDate", TIMESTAMP, server_default=func.now()), + Column("serialId", String(20)), + Column("placeId", Integer), + Column("clientId", String(11)), + Column("printerSerialId", String(20)), Column("cardRomVersion", Integer), Column("isHolograph", Boolean, server_default="1"), Column("printOption1", Boolean, server_default="0"), diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 3cb42d1..eb0da73 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -99,6 +99,68 @@ detail = Table( mysql_charset="utf8mb4", ) +detail_old = Table( + "maimai_profile_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("lastDataVersion", Integer), + Column("userName", String(8)), + Column("point", Integer), + Column("totalPoint", Integer), + Column("iconId", Integer), + Column("nameplateId", Integer), + Column("frameId", Integer), + Column("trophyId", Integer), + Column("playCount", Integer), + Column("playVsCount", Integer), + Column("playSyncCount", Integer), + Column("winCount", Integer), + Column("helpCount", Integer), + Column("comboCount", Integer), + Column("feverCount", Integer), + Column("totalHiScore", Integer), + Column("totalEasyHighScore", Integer), + Column("totalBasicHighScore", Integer), + Column("totalAdvancedHighScore", Integer), + Column("totalExpertHighScore", Integer), + Column("totalMasterHighScore", Integer), + Column("totalReMasterHighScore", Integer), + Column("totalHighSync", Integer), + Column("totalEasySync", Integer), + Column("totalBasicSync", Integer), + Column("totalAdvancedSync", Integer), + Column("totalExpertSync", Integer), + Column("totalMasterSync", Integer), + Column("totalReMasterSync", Integer), + Column("playerRating", Integer), + Column("highestRating", Integer), + Column("rankAuthTailId", Integer), + Column("eventWatchedDate", String(255)), + Column("webLimitDate", String(255)), + Column("challengeTrackPhase", Integer), + Column("firstPlayBits", Integer), + Column("lastPlayDate", String(255)), + Column("lastPlaceId", Integer), + Column("lastPlaceName", String(255)), + Column("lastRegionId", Integer), + Column("lastRegionName", String(255)), + Column("lastClientId", String(255)), + Column("lastCountryCode", String(255)), + Column("eventPoint", Integer), + Column("totalLv", Integer), + Column("lastLoginBonusDay", Integer), + Column("lastSurvivalBonusDay", Integer), + Column("loginBonusLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_detail_uk"), + mysql_charset="utf8mb4", +) + ghost = Table( "mai2_profile_ghost", metadata, @@ -223,6 +285,99 @@ option = Table( mysql_charset="utf8mb4", ) +option_old = Table( + "maimai_profile_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("soudEffect", Integer), + Column("mirrorMode", Integer), + Column("guideSpeed", Integer), + Column("bgInfo", Integer), + Column("brightness", Integer), + Column("isStarRot", Integer), + Column("breakSe", Integer), + Column("slideSe", Integer), + Column("hardJudge", Integer), + Column("isTagJump", Integer), + Column("breakSeVol", Integer), + Column("slideSeVol", Integer), + Column("isUpperDisp", Integer), + Column("trackSkip", Integer), + Column("optionMode", Integer), + Column("simpleOptionParam", Integer), + Column("adjustTiming", Integer), + Column("dispTiming", Integer), + Column("timingPos", Integer), + Column("ansVol", Integer), + Column("noteVol", Integer), + Column("dmgVol", Integer), + Column("appealFlame", Integer), + Column("isFeverDisp", Integer), + Column("dispJudge", Integer), + Column("judgePos", Integer), + Column("ratingGuard", Integer), + Column("selectChara", Integer), + Column("sortType", Integer), + Column("filterGenre", Integer), + Column("filterLevel", Integer), + Column("filterRank", Integer), + Column("filterVersion", Integer), + Column("filterRec", Integer), + Column("filterFullCombo", Integer), + Column("filterAllPerfect", Integer), + Column("filterDifficulty", Integer), + Column("filterFullSync", Integer), + Column("filterReMaster", Integer), + Column("filterMaxFever", Integer), + Column("finalSelectId", Integer), + Column("finalSelectCategory", Integer), + UniqueConstraint("user", "version", name="maimai_profile_option_uk"), + mysql_charset="utf8mb4", +) + +web_opt = Table( + "maimai_profile_web_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("isNetMember", Boolean), + Column("dispRate", Integer), + Column("dispJudgeStyle", Integer), + Column("dispRank", Integer), + Column("dispHomeRanker", Integer), + Column("dispTotalLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_web_option_uk"), + mysql_charset="utf8mb4", +) + +grade_status = Table( + "maimai_profile_grade_status", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gradeVersion", Integer), + Column("gradeLevel", Integer), + Column("gradeSubLevel", Integer), + Column("gradeMaxId", Integer), + UniqueConstraint("user", "gradeVersion", name="maimai_profile_grade_status_uk"), + mysql_charset="utf8mb4", +) + rating = Table( "mai2_profile_rating", metadata, diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 85dff16..b754eb6 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -175,6 +175,102 @@ course = Table( mysql_charset="utf8mb4", ) +playlog_old = Table( + "maimai_playlog", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer), + # Pop access code + Column("orderId", Integer), + Column("sortNumber", Integer), + Column("placeId", Integer), + Column("placeName", String(255)), + Column("country", String(255)), + Column("regionId", Integer), + Column("playDate", String(255)), + Column("userPlayDate", String(255)), + Column("musicId", Integer), + Column("level", Integer), + Column("gameMode", Integer), + Column("rivalNum", Integer), + Column("track", Integer), + Column("eventId", Integer), + Column("isFreeToPlay", Boolean), + Column("playerRating", Integer), + Column("playedUserId1", Integer), + Column("playedUserId2", Integer), + Column("playedUserId3", Integer), + Column("playedUserName1", String(255)), + Column("playedUserName2", String(255)), + Column("playedUserName3", String(255)), + Column("playedMusicLevel1", Integer), + Column("playedMusicLevel2", Integer), + Column("playedMusicLevel3", Integer), + Column("achievement", Integer), + Column("score", Integer), + Column("tapScore", Integer), + Column("holdScore", Integer), + Column("slideScore", Integer), + Column("breakScore", Integer), + Column("syncRate", Integer), + Column("vsWin", Integer), + Column("isAllPerfect", Boolean), + Column("fullCombo", Integer), + Column("maxFever", Integer), + Column("maxCombo", Integer), + Column("tapPerfect", Integer), + Column("tapGreat", Integer), + Column("tapGood", Integer), + Column("tapBad", Integer), + Column("holdPerfect", Integer), + Column("holdGreat", Integer), + Column("holdGood", Integer), + Column("holdBad", Integer), + Column("slidePerfect", Integer), + Column("slideGreat", Integer), + Column("slideGood", Integer), + Column("slideBad", Integer), + Column("breakPerfect", Integer), + Column("breakGreat", Integer), + Column("breakGood", Integer), + Column("breakBad", Integer), + Column("judgeStyle", Integer), + Column("isTrackSkip", Boolean), + Column("isHighScore", Boolean), + Column("isChallengeTrack", Boolean), + Column("challengeLife", Integer), + Column("challengeRemain", Integer), + Column("isAllPerfectPlus", Integer), + mysql_charset="utf8mb4", +) + +best_score_old = Table( + "maimai_score_best", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer), + Column("level", Integer), + Column("playCount", Integer), + Column("achievement", Integer), + Column("scoreMax", Integer), + Column("syncRateMax", Integer), + Column("isAllPerfect", Boolean), + Column("isAllPerfectPlus", Integer), + Column("fullCombo", Integer), + Column("maxFever", Integer), + UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"), + mysql_charset="utf8mb4", +) class Mai2ScoreData(BaseData): def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: From b60cf6258da596af25dec14acc101284904490be Mon Sep 17 00:00:00 2001 From: Midorica Date: Tue, 27 Jun 2023 21:32:46 -0400 Subject: [PATCH 272/589] Dummy defrag match handler for SAO --- titles/sao/base.py | 20 +++ titles/sao/handlers/base.py | 255 +++++++++++++++++++++++++++++++++++- 2 files changed, 273 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 58bc3df..c2ec49d 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1169,3 +1169,23 @@ class SaoBase: #quest/trial_tower_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() + + def handle_cd00(self, request: Any) -> bytes: + #defrag_match/get_defrag_match_basic_data + resp = SaoGetDefragMatchBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_cd02(self, request: Any) -> bytes: + #defrag_match/get_defrag_match_ranking_user_data + resp = SaoGetDefragMatchRankingUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_cd04(self, request: Any) -> bytes: + #defrag_match/get_defrag_match_league_point_ranking_list + resp = SaoGetDefragMatchLeaguePointRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_cd06(self, request: Any) -> bytes: + #defrag_match/get_defrag_match_league_score_ranking_list + resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index eea9850..fcae572 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1821,8 +1821,8 @@ class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): super().__init__(cmd) self.result = 1 self.get_flag = 1 - self.elapsed_days = 1 - self.get_yui_medal_num = 1 + self.elapsed_days = 0 + self.get_yui_medal_num = 0 def make(self) -> bytes: # create a resp struct @@ -2209,5 +2209,256 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetDefragMatchBasicDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.defrag_match_basic_user_data_size = 1 # number of arrays + + self.seed_flag = 1 + self.ad_confirm_flag = 1 + self.total_league_point = 0 + self.have_league_score = 0 + self.class_num = 1 # 1 to 6 + self.hall_of_fame_confirm_flag = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "defrag_match_basic_user_data_size" / Int32ub, # big endian + + "seed_flag" / Int16ub, #short + "ad_confirm_flag" / Int8ul, # result is either 0 or 1 + "total_league_point" / Int32ub, #int + "have_league_score" / Int16ub, #short + "class_num" / Int16ub, #short + "hall_of_fame_confirm_flag" / Int8ul, # result is either 0 or 1 + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + defrag_match_basic_user_data_size=self.defrag_match_basic_user_data_size, + + seed_flag=self.seed_flag, + ad_confirm_flag=self.ad_confirm_flag, + total_league_point=self.total_league_point, + have_league_score=self.have_league_score, + class_num=self.class_num, + hall_of_fame_confirm_flag=self.hall_of_fame_confirm_flag, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetDefragMatchRankingUserDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.ranking_user_data_size = 1 # number of arrays + + self.league_point_rank = 1 + self.league_score_rank = 1 + self.nick_name = "PLAYER" + self.setting_title_id = 20005 # Default saved during profile creation, no changing for those atm + self.favorite_hero_log_id = 101000010 # Default saved during profile creation + self.favorite_hero_log_awakening_stage = 0 + self.favorite_support_log_id = 0 + self.favorite_support_log_awakening_stage = 0 + self.total_league_point = 1 + self.have_league_score = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "ranking_user_data_size" / Int32ub, # big endian + + "league_point_rank" / Int32ub, #int + "league_score_rank" / Int32ub, #int + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "setting_title_id" / Int32ub, #int + "favorite_hero_log_id" / Int32ub, #int + "favorite_hero_log_awakening_stage" / Int16ub, #short + "favorite_support_log_id" / Int32ub, #int + "favorite_support_log_awakening_stage" / Int16ub, #short + "total_league_point" / Int32ub, #int + "have_league_score" / Int16ub, #short + ) + + resp_data = resp_struct.build(dict( + result=self.result, + ranking_user_data_size=self.ranking_user_data_size, + + league_point_rank=self.league_point_rank, + league_score_rank=self.league_score_rank, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + setting_title_id=self.setting_title_id, + favorite_hero_log_id=self.favorite_hero_log_id, + favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, + favorite_support_log_id=self.favorite_support_log_id, + favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, + total_league_point=self.total_league_point, + have_league_score=self.have_league_score, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetDefragMatchLeaguePointRankingListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.ranking_user_data_size = 1 # number of arrays + + self.rank = 1 + self.user_id = "1" + self.store_id = "123" + self.store_name = "ARTEMiS" + self.nick_name = "PLAYER" + self.setting_title_id = 20005 + self.favorite_hero_log_id = 101000010 + self.favorite_hero_log_awakening_stage = 0 + self.favorite_support_log_id = 0 + self.favorite_support_log_awakening_stage = 0 + self.class_num = 1 + self.total_league_point = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "ranking_user_data_size" / Int32ub, # big endian + + "rank" / Int32ub, #int + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[len(self.user_id)], + "store_id_size" / Int32ub, # big endian + "store_id" / Int16ul[len(self.store_id)], + "store_name_size" / Int32ub, # big endian + "store_name" / Int16ul[len(self.store_name)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "setting_title_id" / Int32ub, #int + "favorite_hero_log_id" / Int32ub, #int + "favorite_hero_log_awakening_stage" / Int16ub, #short + "favorite_support_log_id" / Int32ub, #int + "favorite_support_log_awakening_stage" / Int16ub, #short + "class_num" / Int16ub, #short + "total_league_point" / Int32ub, #int + ) + + resp_data = resp_struct.build(dict( + result=self.result, + ranking_user_data_size=self.ranking_user_data_size, + + rank=self.rank, + user_id_size=len(self.user_id) * 2, + user_id=[ord(x) for x in self.user_id], + store_id_size=len(self.store_id) * 2, + store_id=[ord(x) for x in self.store_id], + store_name_size=len(self.store_name) * 2, + store_name=[ord(x) for x in self.store_name], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + setting_title_id=self.setting_title_id, + favorite_hero_log_id=self.favorite_hero_log_id, + favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, + favorite_support_log_id=self.favorite_support_log_id, + favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, + class_num=self.class_num, + total_league_point=self.total_league_point, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetDefragMatchLeagueScoreRankingListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.ranking_user_data_size = 1 # number of arrays + + self.rank = 1 + self.user_id = "1" + self.store_id = "123" + self.store_name = "ARTEMiS" + self.nick_name = "PLAYER" + self.setting_title_id = 20005 + self.favorite_hero_log_id = 101000010 + self.favorite_hero_log_awakening_stage = 0 + self.favorite_support_log_id = 0 + self.favorite_support_log_awakening_stage = 0 + self.class_num = 1 + self.have_league_score = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "ranking_user_data_size" / Int32ub, # big endian + + "rank" / Int32ub, #int + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[len(self.user_id)], + "store_id_size" / Int32ub, # big endian + "store_id" / Int16ul[len(self.store_id)], + "store_name_size" / Int32ub, # big endian + "store_name" / Int16ul[len(self.store_name)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "setting_title_id" / Int32ub, #int + "favorite_hero_log_id" / Int32ub, #int + "favorite_hero_log_awakening_stage" / Int16ub, #short + "favorite_support_log_id" / Int32ub, #int + "favorite_support_log_awakening_stage" / Int16ub, #short + "class_num" / Int16ub, #short + "have_league_score" / Int16ub, #short + ) + + resp_data = resp_struct.build(dict( + result=self.result, + ranking_user_data_size=self.ranking_user_data_size, + + rank=self.rank, + user_id_size=len(self.user_id) * 2, + user_id=[ord(x) for x in self.user_id], + store_id_size=len(self.store_id) * 2, + store_id=[ord(x) for x in self.store_id], + store_name_size=len(self.store_name) * 2, + store_name=[ord(x) for x in self.store_name], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + setting_title_id=self.setting_title_id, + favorite_hero_log_id=self.favorite_hero_log_id, + favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, + favorite_support_log_id=self.favorite_support_log_id, + favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, + class_num=self.class_num, + have_league_score=self.have_league_score, + )) + self.length = len(resp_data) return super().make() + resp_data \ No newline at end of file From 9dd2b4d524cdbc382276ae69d8651ea6c711d595 Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 28 Jun 2023 00:18:02 -0400 Subject: [PATCH 273/589] Adding dummy hero QR code scanning for SAO --- docs/game_specific_info.md | 4 +- titles/sao/base.py | 10 ++ titles/sao/handlers/base.py | 249 ++++++++++++++++++++++++++++++++++++ 3 files changed, 262 insertions(+), 1 deletion(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index d2ae6d8..9bc75a6 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -437,9 +437,11 @@ python dbutils.py --game SDEW upgrade ``` ### Notes -- Co-Op (matching) is not supported +- Defrag Match will crash at loading +- Co-Op Online is not supported - Shop is not functionnal - Player title is currently static and cannot be changed in-game +- QR Card Scanning currently only load a static hero ### Credits for SAO support: diff --git a/titles/sao/base.py b/titles/sao/base.py index c2ec49d..8886a49 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1188,4 +1188,14 @@ class SaoBase: def handle_cd06(self, request: Any) -> bytes: #defrag_match/get_defrag_match_league_score_ranking_list resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_d404(self, request: Any) -> bytes: + #other/bnid_serial_code_check + resp = SaoBnidSerialCodeCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c306(self, request: Any) -> bytes: + #card/scan_qr_quest_profile_card + resp = SaoScanQrQuestProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index fcae572..c634caf 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -2460,5 +2460,254 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): have_league_score=self.have_league_score, )) + self.length = len(resp_data) + return super().make() + resp_data + +class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + + self.result = 1 + self.bnid_item_id = "130050" + self.use_status = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "bnid_item_id_size" / Int32ub, # big endian + "bnid_item_id" / Int16ul[len(self.bnid_item_id)], + "use_status" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + bnid_item_id_size=len(self.bnid_item_id) * 2, + bnid_item_id=[ord(x) for x in self.bnid_item_id], + use_status=self.use_status, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoScanQrQuestProfileCardRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + + # read_profile_card_data + self.profile_card_code = "1234123412341234123" # ID of the QR code + self.nick_name = "PLAYER" + self.rank_num = 1 #short + self.setting_title_id = 20005 #int + self.skill_id = 0 #short + self.hero_log_hero_log_id = 118000230 #int + self.hero_log_log_level = 1 #short + self.hero_log_awakening_stage = 1 #short + + self.hero_log_property1_property_id = 0 #int + self.hero_log_property1_value1 = 0 #int + self.hero_log_property1_value2 = 0 #int + self.hero_log_property2_property_id = 0 #int + self.hero_log_property2_value1 = 0 #int + self.hero_log_property2_value2 = 0 #int + self.hero_log_property3_property_id = 0 #int + self.hero_log_property3_value1 = 0 #int + self.hero_log_property3_value2 = 0 #int + self.hero_log_property4_property_id = 0 #int + self.hero_log_property4_value1 = 0 #int + self.hero_log_property4_value2 = 0 #int + + self.main_weapon_equipment_id = 0 #int + self.main_weapon_enhancement_value = 0 #short + self.main_weapon_awakening_stage = 0 #short + + self.main_weapon_property1_property_id = 0 #int + self.main_weapon_property1_value1 = 0 #int + self.main_weapon_property1_value2 = 0 #int + self.main_weapon_property2_property_id = 0 #int + self.main_weapon_property2_value1 = 0 #int + self.main_weapon_property2_value2 = 0 #int + self.main_weapon_property3_property_id = 0 #int + self.main_weapon_property3_value1 = 0 #int + self.main_weapon_property3_value2 = 0 #int + self.main_weapon_property4_property_id = 0 #int + self.main_weapon_property4_value1 = 0 #int + self.main_weapon_property4_value2 = 0 #int + + self.sub_equipment_equipment_id = 0 #int + self.sub_equipment_enhancement_value = 0 #short + self.sub_equipment_awakening_stage = 0 #short + + self.sub_equipment_property1_property_id = 0 #int + self.sub_equipment_property1_value1 = 0 #int + self.sub_equipment_property1_value2 = 0 #int + self.sub_equipment_property2_property_id = 0 #int + self.sub_equipment_property2_value1 = 0 #int + self.sub_equipment_property2_value2 = 0 #int + self.sub_equipment_property3_property_id = 0 #int + self.sub_equipment_property3_value1 = 0 #int + self.sub_equipment_property3_value2 = 0 #int + self.sub_equipment_property4_property_id = 0 #int + self.sub_equipment_property4_value1 = 0 #int + self.sub_equipment_property4_value2 = 0 #int + + self.holographic_flag = 1 #byte + + def make(self) -> bytes: + #new stuff + + read_profile_card_data_struct = Struct( + "profile_card_code_size" / Int32ub, # big endian + "profile_card_code" / Int16ul[len(self.profile_card_code)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, #short + "setting_title_id" / Int32ub, #int + "skill_id" / Int16ub, #short + "hero_log_hero_log_id" / Int32ub, #int + "hero_log_log_level" / Int16ub, #short + "hero_log_awakening_stage" / Int16ub, #short + + "hero_log_property1_property_id" / Int32ub, #int + "hero_log_property1_value1" / Int32ub, #int + "hero_log_property1_value2" / Int32ub, #int + "hero_log_property2_property_id" / Int32ub, #int + "hero_log_property2_value1" / Int32ub, #int + "hero_log_property2_value2" / Int32ub, #int + "hero_log_property3_property_id" / Int32ub, #int + "hero_log_property3_value1" / Int32ub, #int + "hero_log_property3_value2" / Int32ub, #int + "hero_log_property4_property_id" / Int32ub, #int + "hero_log_property4_value1" / Int32ub, #int + "hero_log_property4_value2" / Int32ub, #int + + "main_weapon_equipment_id" / Int32ub, #int + "main_weapon_enhancement_value" / Int16ub, #short + "main_weapon_awakening_stage" / Int16ub, #short + + "main_weapon_property1_property_id" / Int32ub, #int + "main_weapon_property1_value1" / Int32ub, #int + "main_weapon_property1_value2" / Int32ub, #int + "main_weapon_property2_property_id" / Int32ub, #int + "main_weapon_property2_value1" / Int32ub, #int + "main_weapon_property2_value2" / Int32ub, #int + "main_weapon_property3_property_id" / Int32ub, #int + "main_weapon_property3_value1" / Int32ub, #int + "main_weapon_property3_value2" / Int32ub, #int + "main_weapon_property4_property_id" / Int32ub, #int + "main_weapon_property4_value1" / Int32ub, #int + "main_weapon_property4_value2" / Int32ub, #int + + "sub_equipment_equipment_id" / Int32ub, #int + "sub_equipment_enhancement_value" / Int16ub, #short + "sub_equipment_awakening_stage" / Int16ub, #short + + "sub_equipment_property1_property_id" / Int32ub, #int + "sub_equipment_property1_value1" / Int32ub, #int + "sub_equipment_property1_value2" / Int32ub, #int + "sub_equipment_property2_property_id" / Int32ub, #int + "sub_equipment_property2_value1" / Int32ub, #int + "sub_equipment_property2_value2" / Int32ub, #int + "sub_equipment_property3_property_id" / Int32ub, #int + "sub_equipment_property3_value1" / Int32ub, #int + "sub_equipment_property3_value2" / Int32ub, #int + "sub_equipment_property4_property_id" / Int32ub, #int + "sub_equipment_property4_value1" / Int32ub, #int + "sub_equipment_property4_value2" / Int32ub, #int + + "holographic_flag" / Int8ul, # result is either 0 or 1 + + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "read_profile_card_data_size" / Rebuild(Int32ub, len_(this.read_profile_card_data)), # big endian + "read_profile_card_data" / Array(this.read_profile_card_data_size, read_profile_card_data_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + read_profile_card_data_size=0, + read_profile_card_data=[], + ))) + + hero_data = dict( + profile_card_code_size=len(self.profile_card_code) * 2, + profile_card_code=[ord(x) for x in self.profile_card_code], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + + rank_num=self.rank_num, + setting_title_id=self.setting_title_id, + skill_id=self.skill_id, + hero_log_hero_log_id=self.hero_log_hero_log_id, + hero_log_log_level=self.hero_log_log_level, + hero_log_awakening_stage=self.hero_log_awakening_stage, + + hero_log_property1_property_id=self.hero_log_property1_property_id, + hero_log_property1_value1=self.hero_log_property1_value1, + hero_log_property1_value2=self.hero_log_property1_value2, + hero_log_property2_property_id=self.hero_log_property2_property_id, + hero_log_property2_value1=self.hero_log_property2_value1, + hero_log_property2_value2=self.hero_log_property2_value2, + hero_log_property3_property_id=self.hero_log_property3_property_id, + hero_log_property3_value1=self.hero_log_property3_value1, + hero_log_property3_value2=self.hero_log_property3_value2, + hero_log_property4_property_id=self.hero_log_property4_property_id, + hero_log_property4_value1=self.hero_log_property4_value1, + hero_log_property4_value2=self.hero_log_property4_value2, + + main_weapon_equipment_id=self.main_weapon_equipment_id, + main_weapon_enhancement_value=self.main_weapon_enhancement_value, + main_weapon_awakening_stage=self.main_weapon_awakening_stage, + + main_weapon_property1_property_id=self.main_weapon_property1_property_id, + main_weapon_property1_value1=self.main_weapon_property1_value1, + main_weapon_property1_value2=self.main_weapon_property1_value2, + main_weapon_property2_property_id=self.main_weapon_property2_property_id, + main_weapon_property2_value1=self.main_weapon_property2_value1, + main_weapon_property2_value2=self.main_weapon_property2_value2, + main_weapon_property3_property_id=self.main_weapon_property3_property_id, + main_weapon_property3_value1=self.main_weapon_property3_value1, + main_weapon_property3_value2=self.main_weapon_property3_value2, + main_weapon_property4_property_id=self.main_weapon_property4_property_id, + main_weapon_property4_value1=self.main_weapon_property4_value1, + main_weapon_property4_value2=self.main_weapon_property4_value2, + + sub_equipment_equipment_id=self.sub_equipment_equipment_id, + sub_equipment_enhancement_value=self.sub_equipment_enhancement_value, + sub_equipment_awakening_stage=self.sub_equipment_awakening_stage, + + sub_equipment_property1_property_id=self.sub_equipment_property1_property_id, + sub_equipment_property1_value1=self.sub_equipment_property1_value1, + sub_equipment_property1_value2=self.sub_equipment_property1_value2, + sub_equipment_property2_property_id=self.sub_equipment_property2_property_id, + sub_equipment_property2_value1=self.sub_equipment_property2_value1, + sub_equipment_property2_value2=self.sub_equipment_property2_value2, + sub_equipment_property3_property_id=self.sub_equipment_property3_property_id, + sub_equipment_property3_value1=self.sub_equipment_property3_value1, + sub_equipment_property3_value2=self.sub_equipment_property3_value2, + sub_equipment_property4_property_id=self.sub_equipment_property4_property_id, + sub_equipment_property4_value1=self.sub_equipment_property4_value1, + sub_equipment_property4_value2=self.sub_equipment_property4_value2, + + holographic_flag=self.holographic_flag, + ) + + resp_data.read_profile_card_data.append(hero_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + self.length = len(resp_data) return super().make() + resp_data \ No newline at end of file From e446816b9a26f73ae7e91db91c89a9d3c2eecc2b Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 28 Jun 2023 08:24:53 -0400 Subject: [PATCH 274/589] fixing issue where SaoItemData was not working --- titles/sao/database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/titles/sao/database.py b/titles/sao/database.py index 463440d..b7026fb 100644 --- a/titles/sao/database.py +++ b/titles/sao/database.py @@ -8,5 +8,6 @@ class SaoData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) + self.item = SaoItemData(cfg, self.session) self.profile = SaoProfileData(cfg, self.session) self.static = SaoStaticData(cfg, self.session) \ No newline at end of file From 20389011e9cbd470febf84d43851df582afdf403 Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 28 Jun 2023 12:54:16 -0400 Subject: [PATCH 275/589] Adding proper hero unlock after stage clear on SAO --- titles/sao/base.py | 85 +++++++++++++++++++++++++++++++++++-- titles/sao/handlers/base.py | 4 +- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 8886a49..ad03e97 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1160,14 +1160,91 @@ class SaoBase: resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - def handle_c90a(self, request: Any) -> bytes: #should be tweaked for proper item unlock + def handle_c90a(self, request: Any) -> bytes: #quest/episode_play_end_unanalyzed_log_fixed - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id + "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + + with open('titles/sao/data/RewardTable.csv', 'r') as f: + keys_unanalyzed = next(f).strip().split(',') + data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + + randomized_unanalyzed_id = choice(data_unanalyzed) + heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + i = 0 + + # Create a loop to check if the id is a hero or else try 15 times before closing the loop and sending a dummy hero + while not heroList: + if i == 15: + # Return the dummy hero but not save it + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, 102000070) + return resp.make() + + i += 1 + randomized_unanalyzed_id = choice(data_unanalyzed) + heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + + hero_data = self.game_data.item.get_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId']) + + # Avoid having a duplicated card and cause an overwrite + if not hero_data: + self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, randomized_unanalyzed_id['CommonRewardId']) return resp.make() def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(16), + "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id + "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + + with open('titles/sao/data/RewardTable.csv', 'r') as f: + keys_unanalyzed = next(f).strip().split(',') + data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + + randomized_unanalyzed_id = choice(data_unanalyzed) + heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + i = 0 + + # Create a loop to check if the id is a hero or else try 15 times before closing the loop and sending a dummy hero + while not heroList: + if i == 15: + # Return the dummy hero but not save it + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, 102000070) + return resp.make() + + i += 1 + randomized_unanalyzed_id = choice(data_unanalyzed) + heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) + + hero_data = self.game_data.item.get_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId']) + + # Avoid having a duplicated card and cause an overwrite + if not hero_data: + self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) + + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, randomized_unanalyzed_id['CommonRewardId']) return resp.make() def handle_cd00(self, request: Any) -> bytes: @@ -1189,7 +1266,7 @@ class SaoBase: #defrag_match/get_defrag_match_league_score_ranking_list resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - + def handle_d404(self, request: Any) -> bytes: #other/bnid_serial_code_check resp = SaoBnidSerialCodeCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index c634caf..f5c114f 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1661,7 +1661,7 @@ class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): super().__init__(data) class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: + def __init__(self, cmd, randomized_unanalyzed_id) -> None: super().__init__(cmd) self.result = 1 self.play_end_unanalyzed_log_reward_data_list_size = 1 # Number of arrays @@ -1670,7 +1670,7 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): self.common_reward_data_size = 1 self.common_reward_type_1 = 1 - self.common_reward_id_1 = 102000070 + self.common_reward_id_1 = int(randomized_unanalyzed_id) self.common_reward_num_1 = 1 def make(self) -> bytes: From 4ea83f60256a21c5c42b690f80fd27b496b10a67 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 00:26:07 -0400 Subject: [PATCH 276/589] allnet: add handler for LoaderStateRecorder --- core/allnet.py | 18 +++++++++++++++++- index.py | 7 +++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 8af120b..00303f5 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Tuple +from typing import Dict, List, Any, Optional, Tuple, Union import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web.http import Request @@ -241,6 +241,22 @@ class AllnetServlet: ) return b"" + def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: + req_data = request.content.getvalue() + req_dict = self.kvp_to_dict([req_data.decode()])[0] + + serial: Union[str, None] = req_dict.get("serial", None) + num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None) + num_files_dld: Union[str, None] = req_dict.get("nb_dld", None) + dl_state: Union[str, None] = req_dict.get("dld_st", None) + ip = Utils.get_ip_addr(request) + + if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: + return "NG".encode() + + self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") + return "OK".encode() + def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) request_ip = Utils.get_ip_addr(request) diff --git a/index.py b/index.py index 7199dbe..265bded 100644 --- a/index.py +++ b/index.py @@ -63,6 +63,13 @@ class HttpDispatcher(resource.Resource): action="handle_dlorder", conditions=dict(method=["POST"]), ) + self.map_post.connect( + "allnet_loaderstaterecorder", + "/sys/servlet/LoaderStateRecorder", + controller="allnet", + action="handle_loaderstaterecorder", + conditions=dict(method=["POST"]), + ) self.map_post.connect( "allnet_billing", "/request", From 610ef70bad3de6b83c4ca09013c26e91ef6578e6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 00:32:52 -0400 Subject: [PATCH 277/589] allnet: add Alive get and post handlers --- core/allnet.py | 3 +++ index.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 00303f5..edb704c 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -256,6 +256,9 @@ class AllnetServlet: self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") return "OK".encode() + + def handle_alive(self, request: Request, match: Dict) -> bytes: + return "OK".encode() def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) diff --git a/index.py b/index.py index 265bded..6996f0e 100644 --- a/index.py +++ b/index.py @@ -70,6 +70,20 @@ class HttpDispatcher(resource.Resource): action="handle_loaderstaterecorder", conditions=dict(method=["POST"]), ) + self.map_post.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["POST"]), + ) + self.map_get.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["GET"]), + ) self.map_post.connect( "allnet_billing", "/request", From 8b43d554fc35cf26ff1b9a68f631d9f79dad682a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 01:19:17 -0400 Subject: [PATCH 278/589] allnet: make use of urllib.parse where applicable --- core/allnet.py | 137 +++++++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index edb704c..bc5eedf 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -11,6 +11,7 @@ from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 from time import strptime from os import path +import urllib.parse from core.config import CoreConfig from core.utils import Utils @@ -79,7 +80,7 @@ class AllnetServlet: req = AllnetPowerOnRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.serial or not req.ip: + if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver: raise AllnetRequestException( f"Bad auth request params from {request_ip} - {vars(req)}" ) @@ -89,12 +90,14 @@ class AllnetServlet: self.logger.error(e) return b"" - if req.format_ver == "3": + if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) - else: + elif req.format_ver == 2: resp = AllnetPowerOnResponse2() + else: + resp = AllnetPowerOnResponse() - self.logger.debug(f"Allnet request: {vars(req)}") + self.logger.debug(f"Allnet request: {vars(req)}") if req.game_id not in self.uri_registry: if not self.config.server.is_develop: msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." @@ -103,8 +106,9 @@ class AllnetServlet: ) self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = -1 + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") else: self.logger.info( @@ -113,12 +117,15 @@ class AllnetServlet: resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" - self.logger.debug(f"Allnet response: {vars(resp)}") - return self.dict_to_http_form_string([vars(resp)]) + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + + self.logger.debug(f"Allnet response: {resp_str}") + return (resp_str + "\n").encode("utf-8") resp.uri, resp.host = self.uri_registry[req.game_id] - machine = self.data.arcade.get_machine(req.serial) + machine = self.data.arcade.get_machine(req.serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." self.data.base.log_event( @@ -126,8 +133,9 @@ class AllnetServlet: ) self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = -2 + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) @@ -169,9 +177,13 @@ class AllnetServlet: 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) - self.logger.debug(f"Allnet response: {vars(resp)}") - return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + self.logger.debug(f"Allnet response: {resp_dict}") + resp_str += "\n" + + return resp_str.encode("utf-8") def handle_dlorder(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) @@ -202,7 +214,7 @@ class AllnetServlet: not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder ): - return self.dict_to_http_form_string([vars(resp)]) + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" else: # TODO: Keychip check if path.exists( @@ -217,7 +229,8 @@ class AllnetServlet: self.logger.debug(f"Sending download uri {resp.uri}") self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") - return self.dict_to_http_form_string([vars(resp)]) + + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: if "file" not in match: @@ -323,7 +336,7 @@ class AllnetServlet: resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) - resp_str = self.dict_to_http_form_string([vars(resp)], True) + resp_str = self.dict_to_http_form_string([vars(resp)]) if resp_str is None: self.logger.error(f"Failed to parse response {vars(resp)}") @@ -382,7 +395,7 @@ class AllnetServlet: def dict_to_http_form_string( self, data: List[Dict[str, Any]], - crlf: bool = False, + crlf: bool = True, trailing_newline: bool = True, ) -> Optional[str]: """ @@ -392,21 +405,19 @@ class AllnetServlet: urlencode = "" for item in data: for k, v in item.items(): + if k is None or v is None: + continue 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] else: urlencode = urlencode[:-1] - return urlencode - except Exception as e: self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}") return None @@ -416,20 +427,19 @@ class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: raise AllnetRequestException("Request processing failed") - 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 = req.get("format_ver", "2") - self.token = int(req.get("token", "0")) + self.game_id: str = req.get("game_id", None) + self.ver: str = req.get("ver", None) + self.serial: str = req.get("serial", None) + self.ip: str = req.get("ip", None) + self.firm_ver: str = req.get("firm_ver", None) + self.boot_ver: str = req.get("boot_ver", None) + self.encode: str = req.get("encode", "EUC-JP") + self.hops = int(req.get("hops", "-1")) + self.format_ver = float(req.get("format_ver", "1.00")) + self.token: str = req.get("token", "0") - -class AllnetPowerOnResponse3: - def __init__(self, token) -> None: +class AllnetPowerOnResponse: + def __init__(self) -> None: self.stat = 1 self.uri = "" self.host = "" @@ -440,40 +450,45 @@ class AllnetPowerOnResponse3: self.region_name0 = "W" self.region_name1 = "" self.region_name2 = "" - self.region_name3 = "" - self.country = "JPN" - self.allnet_id = "123" - self.client_timezone = "+0900" - self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) + self.region_name3 = "" self.setting = "1" - self.res_ver = "3" - self.token = str(token) - - -class AllnetPowerOnResponse2: - def __init__(self) -> None: - self.stat = 1 - self.uri = "" - self.host = "" - self.place_id = "123" - self.name = "ARTEMiS" - self.nickname = "ARTEMiS" - self.region0 = "1" - self.region_name0 = "W" - self.region_name1 = "X" - self.region_name2 = "Y" - self.region_name3 = "Z" - self.country = "JPN" self.year = datetime.now().year self.month = datetime.now().month self.day = datetime.now().day self.hour = datetime.now().hour self.minute = datetime.now().minute self.second = datetime.now().second - self.setting = "1" - self.timezone = "+0900" + +class AllnetPowerOnResponse3(AllnetPowerOnResponse): + def __init__(self, token) -> None: + super().__init__() + + # Added in v3 + self.country = "JPN" + self.allnet_id = "123" + self.client_timezone = "+0900" + self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + self.res_ver = "3" + self.token = token + + # Removed in v3 + self.year = None + self.month = None + self.day = None + self.hour = None + self.minute = None + self.second = None + + +class AllnetPowerOnResponse2(AllnetPowerOnResponse): + def __init__(self) -> None: + super().__init__() + + # Added in v2 + self.country = "JPN" + self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" From 9d33091bb85cc43749a800a862b86b0a69a1a682 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 01:34:46 -0400 Subject: [PATCH 279/589] allnet: use parse_qsl --- core/allnet.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index bc5eedf..d440b0f 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -256,7 +256,9 @@ class AllnetServlet: def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: req_data = request.content.getvalue() - req_dict = self.kvp_to_dict([req_data.decode()])[0] + sections = req_data.decode("utf-8").split("\r\n") + + req_dict = dict(urllib.parse.parse_qsl(sections[0])) serial: Union[str, None] = req_dict.get("serial", None) num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None) @@ -347,21 +349,6 @@ class AllnetServlet: 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]]: - ret: List[Dict[str, Any]] = [] - for x in kvp: - items = x.split("&") - tmp = {} - - for item in items: - 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): """ Parses an billing request string into a python dictionary @@ -371,7 +358,10 @@ class AllnetServlet: unzipped = decomp.decompress(data) sections = unzipped.decode("ascii").split("\r\n") - return self.kvp_to_dict(sections) + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret except Exception as e: self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") @@ -386,7 +376,10 @@ class AllnetServlet: unzipped = zlib.decompress(zipped) sections = unzipped.decode("utf-8").split("\r\n") - return self.kvp_to_dict(sections) + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret except Exception as e: self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") From 318b73dd57b71dfb87fabef9c21ce99f1c2b222e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:08:54 -0400 Subject: [PATCH 280/589] finale: finish porting request data from aqua --- titles/mai2/base.py | 92 +++++++++++++---- titles/mai2/dx.py | 21 ---- titles/mai2/schema/profile.py | 182 +++++++++++++++++++++++++++++----- titles/mai2/schema/score.py | 39 +++++--- 4 files changed, 258 insertions(+), 76 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ef15abe..ae12e81 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -112,12 +112,12 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - o = self.data.profile.get_profile_option(data["userId"], self.version) - if p is None or o is None: + p = self.data.profile.get_profile_detail(data["userId"], self.version, True) + w = self.data.profile.get_web_option(data["userId"], self.version) + if p is None or w is None: return {} # Register profile = p._asdict() - option = o._asdict() + web_opt = w._asdict() return { "userId": data["userId"], @@ -127,16 +127,15 @@ class Mai2Base: "lastLoginDate": profile["lastLoginDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused + "nameplateId": profile["nameplateId"], "frameId": profile["frameId"], "iconId": profile["iconId"], - "trophyId": 0, # Unused - "partnerId": profile["partnerId"], - "dispRate": option["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide - "dispRank": 0, # TODO - "dispHomeRanker": 0, # TODO - "dispTotalLv": 0, # TODO - "totalLv": 0, # TODO + "trophyId": profile["trophyId"], + "dispRate": web_opt["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide + "dispRank": web_opt["dispRank"], + "dispHomeRanker": web_opt["dispHomeRanker"], + "dispTotalLv": web_opt["dispTotalLv"], + "totalLv": profile["totalLv"], } def handle_user_login_api_request(self, data: Dict) -> Dict: @@ -188,11 +187,34 @@ class Mai2Base: upsert = data["upsertUserAll"] if "userData" in upsert and len(upsert["userData"]) > 0: - upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") + upsert["userData"][0].pop("userId") + self.data.profile.put_profile_detail( - user_id, self.version, upsert["userData"][0] + user_id, self.version, upsert["userData"][0], False ) + + if "UserWebOption" in upsert and len(upsert["UserWebOption"]) > 0: + upsert["UserWebOption"][0]["isNetMember"] = True + self.data.profile.put_web_option( + user_id, self.version, upsert["UserWebOption"][0] + ) + + if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: + self.data.profile.put_web_option( + user_id, self.version, upsert["userGradeStatusList"][0] + ) + + if "userBossList" in upsert and len(upsert["userBossList"]) > 0: + self.data.profile.put_boss_list( + user_id, self.version, upsert["userBossList"][0] + ) + + if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: + for playlog in upsert["userPlaylogList"]: + self.data.score.put_playlog( + user_id, self.version, playlog + ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: self.data.profile.put_profile_extend( @@ -201,11 +223,14 @@ class Mai2Base: if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_extend(user_id, self.version, ghost) + self.data.profile.put_profile_ghost(user_id, self.version, ghost) + + if "userRecentRatingList" in upsert: + self.data.profile.put_recent_rating(user_id, self.version, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_option( - user_id, self.version, upsert["userOption"][0] + user_id, self.version, upsert["userOption"][0], False ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: @@ -305,7 +330,7 @@ class Mai2Base: return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) + profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) if profile is None: return @@ -329,7 +354,7 @@ class Mai2Base: 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) + options = self.data.profile.get_profile_option(data["userId"], self.version, False) if options is None: return @@ -399,6 +424,25 @@ class Mai2Base: "userChargeList": user_charge_list, } + def handle_get_user_present_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + + def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + return {} + + def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []} + + def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + b = self.data.profile.get_boss_list(data["userId"]) + if b is None: + return { "userId": data.get("userId", 0), "userBossData": {}} + boss_lst = b._asdict() + boss_lst.pop("id") + boss_lst.pop("user") + + return { "userId": data.get("userId", 0), "userBossData": boss_lst} + def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) @@ -435,6 +479,8 @@ class Mai2Base: tmp = chara._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("awakening") + tmp.pop("useCount") chara_list.append(tmp) return {"userId": data["userId"], "userCharacterList": chara_list} @@ -468,6 +514,16 @@ class Mai2Base: return {"userId": data["userId"], "userGhost": ghost_dict} + def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_recent_rating(data["userId"]) + if rating is None: + return + + r = rating._asdict() + lst = r.get("userRecentRatingList", []) + + return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst} + def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9ac7067..266332e 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -75,27 +75,6 @@ class Mai2DX(Mai2Base): 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) - - if profile is not None: - lastLoginDate = profile["lastLoginDate"] - loginCt = profile["playCount"] - - if "regionId" in data: - self.data.profile.put_profile_region(data["userId"], data["regionId"]) - else: - loginCt = 0 - lastLoginDate = "2017-12-05 07:00:00.0" - - return { - "returnCode": 1, - "lastLoginDate": lastLoginDate, - "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... - "loginId": loginCt, # Used with the playlog! - } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index eb0da73..950fd99 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -423,42 +423,80 @@ activity = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("kind", Integer, nullable=False), - Column("activityId", Integer, nullable=False), - Column("param1", Integer, nullable=False), - Column("param2", Integer, nullable=False), - Column("param3", Integer, nullable=False), - Column("param4", Integer, nullable=False), - Column("sortNumber", Integer, nullable=False), + Column("kind", Integer), + Column("activityId", Integer), + Column("param1", Integer), + Column("param2", Integer), + Column("param3", Integer), + Column("param4", Integer), + Column("sortNumber", Integer), UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"), mysql_charset="utf8mb4", ) +boss_list = Table( + "mai2_profile_boss_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("pandoraFlagList0", Integer), + Column("pandoraFlagList1", Integer), + Column("pandoraFlagList2", Integer), + Column("pandoraFlagList3", Integer), + Column("pandoraFlagList4", Integer), + Column("pandoraFlagList5", Integer), + Column("pandoraFlagList6", Integer), + Column("emblemFlagList", Integer), + UniqueConstraint("user", name="mai2_profile_boss_list_uk"), + mysql_charset="utf8mb4", +) + +recent_rating = Table( + "mai2_profile_recent_rating", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("userRecentRatingList", Integer), + UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): def put_profile_detail( - self, user_id: int, version: int, detail_data: Dict + self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: detail_data["user"] = user_id detail_data["version"] = version - sql = insert(detail).values(**detail_data) + + if is_dx: + sql = insert(detail).values(**detail_data) + else: + sql = insert(detail_old).values(**detail_data) conflict = sql.on_duplicate_key_update(**detail_data) result = self.execute(conflict) if result is None: self.logger.warn( - f"put_profile: Failed to create profile! user_id {user_id}" + f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}" ) return None return result.lastrowid - def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: - sql = ( - select(detail) - .where(and_(detail.c.user == user_id, detail.c.version <= version)) - .order_by(detail.c.version.desc()) - ) + def get_profile_detail(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: + if is_dx: + sql = ( + select(detail) + .where(and_(detail.c.user == user_id, detail.c.version <= version)) + .order_by(detail.c.version.desc()) + ) + + else: + sql = ( + select(detail_old) + .where(and_(detail_old.c.user == user_id, detail_old.c.version <= version)) + .order_by(detail_old.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -520,26 +558,36 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_profile_option( - self, user_id: int, version: int, option_data: Dict + self, user_id: int, version: int, option_data: Dict, is_dx: bool = True ) -> Optional[int]: option_data["user"] = user_id option_data["version"] = version - sql = insert(option).values(**option_data) + if is_dx: + sql = insert(option).values(**option_data) + else: + sql = insert(option_old).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_option: failed to update! {user_id}") + self.logger.warn(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}") return None return result.lastrowid - def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: - sql = ( - select(option) - .where(and_(option.c.user == user_id, option.c.version <= version)) - .order_by(option.c.version.desc()) - ) + def get_profile_option(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: + if is_dx: + sql = ( + select(option) + .where(and_(option.c.user == user_id, option.c.version <= version)) + .order_by(option.c.version.desc()) + ) + else: + sql = ( + select(option_old) + .where(and_(option_old.c.user == user_id, option_old.c.version <= version)) + .order_by(option_old.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -629,3 +677,87 @@ class Mai2ProfileData(BaseData): if result is None: return None return result.fetchall() + + def put_web_option(self, user_id: int, web_opts: Dict) -> Optional[int]: + sql = insert(web_opt).values(**web_opts) + + conflict = sql.on_duplicate_key_update(**web_opts) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_web_option: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_web_option(self, user_id: int) -> Optional[Row]: + sql = web_opt.select(web_opt.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + sql = insert(grade_status).values(**grade_stat) + + conflict = sql.on_duplicate_key_update(**grade_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_grade_status: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_grade_status(self, user_id: int) -> Optional[Row]: + sql = grade_status.select(grade_status.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + sql = insert(boss_list).values(**boss_stat) + + conflict = sql.on_duplicate_key_update(**boss_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_boss_list: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_boss_list(self, user_id: int) -> Optional[Row]: + sql = boss_list.select(boss_list.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: + sql = insert(recent_rating).values(**rr) + + conflict = sql.on_duplicate_key_update(**rr) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_recent_rating: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_recent_rating(self, user_id: int) -> Optional[Row]: + sql = recent_rating.select(recent_rating.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index b754eb6..6b25d14 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -273,28 +273,39 @@ best_score_old = Table( ) class Mai2ScoreData(BaseData): - def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: + def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]: score_data["user"] = user_id - sql = insert(best_score).values(**score_data) + if is_dx: + sql = insert(best_score).values(**score_data) + else: + sql = insert(best_score_old).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) result = self.execute(conflict) if result is None: self.logger.error( - f"put_best_score: Failed to insert best score! user_id {user_id}" + f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}" ) return None return result.lastrowid @cached(2) - 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, + def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]: + if is_dx: + sql = best_score.select( + and_( + best_score.c.user == user_id, + (best_score.c.song_id == song_id) if song_id is not None else True, + ) + ) + else: + sql = best_score_old.select( + and_( + best_score_old.c.user == user_id, + (best_score_old.c.song_id == song_id) if song_id is not None else True, + ) ) - ) result = self.execute(sql) if result is None: @@ -317,15 +328,19 @@ class Mai2ScoreData(BaseData): return None return result.fetchone() - def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: + def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: playlog_data["user"] = user_id - sql = insert(playlog).values(**playlog_data) + + if is_dx: + sql = insert(playlog).values(**playlog_data) + else: + sql = insert(playlog_old).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) result = self.execute(conflict) if result is None: - self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}") + self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid From 2c6902a546672187b07b075d2504ccaaa0bfc584 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:12:15 -0400 Subject: [PATCH 281/589] mai2: fix typos --- titles/mai2/schema/profile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 950fd99..f423b34 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -434,8 +434,8 @@ activity = Table( mysql_charset="utf8mb4", ) -boss_list = Table( - "mai2_profile_boss_list", +boss = Table( + "maimai_profile_boss", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), @@ -447,12 +447,12 @@ boss_list = Table( Column("pandoraFlagList5", Integer), Column("pandoraFlagList6", Integer), Column("emblemFlagList", Integer), - UniqueConstraint("user", name="mai2_profile_boss_list_uk"), + UniqueConstraint("user", name="mai2_profile_boss_uk"), mysql_charset="utf8mb4", ) recent_rating = Table( - "mai2_profile_recent_rating", + "maimai_profile_recent_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), @@ -721,7 +721,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: - sql = insert(boss_list).values(**boss_stat) + sql = insert(boss).values(**boss_stat) conflict = sql.on_duplicate_key_update(**boss_stat) @@ -734,7 +734,7 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_boss_list(self, user_id: int) -> Optional[Row]: - sql = boss_list.select(boss_list.c.user == user_id) + sql = boss.select(boss.c.user == user_id) result = self.execute(sql) if result is None: From 3e461f4d71edfe13b010d2d8b831c86f0a145bc6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:41:34 -0400 Subject: [PATCH 282/589] mai2: finale fixes --- titles/mai2/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ae12e81..32f7922 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -113,7 +113,7 @@ class Mai2Base: def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version, True) - w = self.data.profile.get_web_option(data["userId"], self.version) + w = self.data.profile.get_web_option(data["userId"]) if p is None or w is None: return {} # Register profile = p._asdict() @@ -201,19 +201,19 @@ class Mai2Base: ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: - self.data.profile.put_web_option( - user_id, self.version, upsert["userGradeStatusList"][0] + self.data.profile.put_grade_status( + user_id, upsert["userGradeStatusList"][0] ) if "userBossList" in upsert and len(upsert["userBossList"]) > 0: self.data.profile.put_boss_list( - user_id, self.version, upsert["userBossList"][0] + user_id, upsert["userBossList"][0] ) if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: for playlog in upsert["userPlaylogList"]: self.data.score.put_playlog( - user_id, self.version, playlog + user_id, playlog ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: From dc8c27046ef8eca9f6bfbd8f2f8369ce5f3e3b44 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:42:38 -0400 Subject: [PATCH 283/589] mai2: more finale fixes --- titles/mai2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 32f7922..6f68487 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -226,7 +226,7 @@ class Mai2Base: self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userRecentRatingList" in upsert: - self.data.profile.put_recent_rating(user_id, self.version, upsert["userRecentRatingList"]) + self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_option( From d89eb61e6276dc9f625cc7caf1a8c3fec5e2a1d5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:56:52 -0400 Subject: [PATCH 284/589] mai2: fixes round 3 --- titles/mai2/base.py | 14 ++++++-------- titles/mai2/schema/profile.py | 10 +++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 6f68487..e3fce20 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -113,7 +113,7 @@ class Mai2Base: def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version, True) - w = self.data.profile.get_web_option(data["userId"]) + w = self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: return {} # Register profile = p._asdict() @@ -229,6 +229,7 @@ class Mai2Base: self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: + upsert["userOption"][0].pop("userId") self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0], False ) @@ -239,9 +240,8 @@ class Mai2Base: ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: - for k, v in upsert["userActivityList"][0].items(): - for act in v: - self.data.profile.put_profile_activity(user_id, act) + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity(user_id, act) if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: @@ -265,8 +265,7 @@ class Mai2Base: user_id, char["characterId"], char["level"], - char["awakening"], - char["useCount"], + char["point"], ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: @@ -276,7 +275,6 @@ class Mai2Base: int(item["itemKind"]), item["itemId"], item["stock"], - item["isValid"], ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: @@ -302,7 +300,7 @@ class Mai2Base: if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, music) + self.data.score.put_best_score(user_id, music, False) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index f423b34..9fd11a3 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -678,7 +678,9 @@ class Mai2ProfileData(BaseData): return None return result.fetchall() - def put_web_option(self, user_id: int, web_opts: Dict) -> Optional[int]: + def put_web_option(self, user_id: int, version: int, web_opts: Dict) -> Optional[int]: + web_opts["user"] = user_id + web_opts["version"] = version sql = insert(web_opt).values(**web_opts) conflict = sql.on_duplicate_key_update(**web_opts) @@ -691,8 +693,8 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_web_option(self, user_id: int) -> Optional[Row]: - sql = web_opt.select(web_opt.c.user == user_id) + def get_web_option(self, user_id: int, version: int) -> Optional[Row]: + sql = web_opt.select(and_(web_opt.c.user == user_id, web_opt.c.version == version)) result = self.execute(sql) if result is None: @@ -700,6 +702,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + grade_stat["user"] = user_id sql = insert(grade_status).values(**grade_stat) conflict = sql.on_duplicate_key_update(**grade_stat) @@ -721,6 +724,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + boss_stat["user"] = user_id sql = insert(boss).values(**boss_stat) conflict = sql.on_duplicate_key_update(**boss_stat) From 9859ab4fdb7ccf08f2079f8cb707f2d9b0ebd296 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:59:19 -0400 Subject: [PATCH 285/589] mai2: fix playlog saving --- titles/mai2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index e3fce20..5811735 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -213,7 +213,7 @@ class Mai2Base: if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: for playlog in upsert["userPlaylogList"]: self.data.score.put_playlog( - user_id, playlog + user_id, playlog, False ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: From d9a92f58650282fb0188e8619d9e648588048ab8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:04:30 -0400 Subject: [PATCH 286/589] mai2: 4th round of fixes --- titles/mai2/base.py | 7 +++---- titles/mai2/schema/item.py | 13 +++++++++++++ titles/mai2/schema/profile.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5811735..8fa3d79 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -261,11 +261,9 @@ class Mai2Base: if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character( + self.data.item.put_character_( user_id, - char["characterId"], - char["level"], - char["point"], + char ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: @@ -275,6 +273,7 @@ class Mai2Base: int(item["itemKind"]), item["itemId"], item["stock"], + True ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 4e20383..284365f 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -333,6 +333,19 @@ class Mai2ItemData(BaseData): if result is None: return None return result.fetchone() + + def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: + char_data["user"] = user_id + sql = insert(character).values(**char_data) + + conflict = sql.on_duplicate_key_update(**char_data) + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_character_: failed to insert item! user_id: {user_id}" + ) + return None + return result.lastrowid def put_character( self, diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 9fd11a3..462e238 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -456,7 +456,7 @@ recent_rating = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("userRecentRatingList", Integer), + Column("userRecentRatingList", JSON), UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), mysql_charset="utf8mb4", ) From b29cb0fbaa29d8b097fcd002e5ae8990a1bcd7b2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:06:00 -0400 Subject: [PATCH 287/589] mai2: fix put_recent_rating --- titles/mai2/schema/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 462e238..48a15ed 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -746,9 +746,9 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: - sql = insert(recent_rating).values(**rr) + sql = insert(recent_rating).values(rr) - conflict = sql.on_duplicate_key_update(**rr) + conflict = sql.on_duplicate_key_update(rr) result = self.execute(conflict) if result is None: From 8f9584c3d2485f3d87f55fb032cd7fa1de574ab0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:07:19 -0400 Subject: [PATCH 288/589] mai2: hotfix put_recent_rating --- titles/mai2/schema/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 48a15ed..f32b877 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -746,9 +746,9 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: - sql = insert(recent_rating).values(rr) + sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) - conflict = sql.on_duplicate_key_update(rr) + conflict = sql.on_duplicate_key_update(userRecentRatingList=rr) result = self.execute(conflict) if result is None: From 3e9cec3a20c8abaa9f7c0f8796a8cab98fccfb07 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:11:37 -0400 Subject: [PATCH 289/589] mai2: put_recent_rating final fix --- titles/mai2/schema/profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index f32b877..54bda21 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -748,7 +748,7 @@ class Mai2ProfileData(BaseData): def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) - conflict = sql.on_duplicate_key_update(userRecentRatingList=rr) + conflict = sql.on_duplicate_key_update({'userRecentRatingList': rr}) result = self.execute(conflict) if result is None: From c4c0566cd5d437161f3d8da258f6ddee596dccdd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:19:19 -0400 Subject: [PATCH 290/589] mai2: fix userWebOption --- titles/mai2/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8fa3d79..8445ab5 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -194,10 +194,10 @@ class Mai2Base: user_id, self.version, upsert["userData"][0], False ) - if "UserWebOption" in upsert and len(upsert["UserWebOption"]) > 0: - upsert["UserWebOption"][0]["isNetMember"] = True + if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: + upsert["userWebOption"][0]["isNetMember"] = True self.data.profile.put_web_option( - user_id, self.version, upsert["UserWebOption"][0] + user_id, self.version, upsert["userWebOption"][0] ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: From 042440c76ed149a0480d9cfd72c3e4ae380ceeb9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:27:26 -0400 Subject: [PATCH 291/589] mai2: fix handle_get_user_preview_api_request --- titles/mai2/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8445ab5..5894282 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -112,7 +112,7 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version, True) + p = self.data.profile.get_profile_detail(data["userId"], self.version, False) w = self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: return {} # Register @@ -124,7 +124,7 @@ class Mai2Base: "userName": profile["userName"], "isLogin": False, "lastDataVersion": profile["lastDataVersion"], - "lastLoginDate": profile["lastLoginDate"], + "lastLoginDate": profile["lastPlayDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], "nameplateId": profile["nameplateId"], From d204954447127c9a196503dd25f5fc41009c1ff1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:40:07 -0400 Subject: [PATCH 292/589] mai2: add missing finale endpoints --- titles/mai2/base.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5894282..a53e416 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -683,6 +683,31 @@ class Mai2Base: def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: + w = self.data.profile.get_web_option(data["userId"], self.version) + if w is None: + return {"userId": data["userId"], "userWebOption": {}} + + web_opt = w._asdict() + web_opt.pop("id") + web_opt.pop("user") + web_opt.pop("version") + + return {"userId": data["userId"], "userWebOption": web_opt} + + def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "length": 0, "userSurvivalList": []} + + def handle_get_user_grade_api_request(self, data: Dict) -> Dict: + g = self.data.profile.get_grade_status(data["userId"]) + if g is None: + return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []} + grade_stat = g._asdict() + grade_stat.pop("id") + grade_stat.pop("user") + + return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) @@ -695,7 +720,7 @@ class Mai2Base: self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - songs = self.data.score.get_best_scores(user_id) + songs = self.data.score.get_best_scores(user_id, is_dx=False) if songs is None: self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { From f279adb89472031be53e53b0a71c9d1d539df676 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 21:51:18 -0400 Subject: [PATCH 293/589] mai2: add consecutive day login count, update db to v7, fix reader, courses, and docs --- core/data/schema/versions/SDEZ_6_rollback.sql | 1 + core/data/schema/versions/SDEZ_7_upgrade.sql | 9 ++++ docs/game_specific_info.md | 45 ++++++++-------- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 29 ++++++++++- titles/mai2/read.py | 3 +- titles/mai2/schema/profile.py | 52 ++++++++++++++++++- titles/mai2/schema/score.py | 2 +- 8 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_6_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_7_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_6_rollback.sql b/core/data/schema/versions/SDEZ_6_rollback.sql new file mode 100644 index 0000000..0ca7036 --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_rollback.sql @@ -0,0 +1 @@ +DROP TABLE aime.mai2_profile_consec_logins; diff --git a/core/data/schema/versions/SDEZ_7_upgrade.sql b/core/data/schema/versions/SDEZ_7_upgrade.sql new file mode 100644 index 0000000..20f3c70 --- /dev/null +++ b/core/data/schema/versions/SDEZ_7_upgrade.sql @@ -0,0 +1,9 @@ +CREATE TABLE `mai2_profile_consec_logins` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `version` int(11) NOT NULL, + `logins` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mai2_profile_consec_logins_uk` (`user`,`version`), + CONSTRAINT `mai2_profile_consec_logins_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f4fec94..0b522aa 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -129,30 +129,31 @@ Config file is located in `config/cxb.yaml`. | Game Code | Version ID | Version Name | |-----------|------------|-------------------------| -| SDEZ | 0 | maimai DX | -| SDEZ | 1 | maimai DX PLUS | -| SDEZ | 2 | maimai DX Splash | -| SDEZ | 3 | maimai DX Splash PLUS | -| SDEZ | 4 | maimai DX Universe | -| SDEZ | 5 | maimai DX Universe PLUS | -| SDEZ | 6 | maimai DX Festival | + For versions pre-dx -| Game Code | Version ID | Version Name | -|-----------|------------|----------------------| -| SBXL | 1000 | maimai | -| SBXL | 1001 | maimai PLUS | -| SBZF | 1002 | maimai GreeN | -| SBZF | 1003 | maimai GreeN PLUS | -| SDBM | 1004 | maimai ORANGE | -| SDBM | 1005 | maimai ORANGE PLUS | -| SDCQ | 1006 | maimai PiNK | -| SDCQ | 1007 | maimai PiNK PLUS | -| SDDK | 1008 | maimai MURASAKI | -| SDDK | 1009 | maimai MURASAKI PLUS | -| SDDZ | 1010 | maimai MILK | -| SDDZ | 1011 | maimai MILK PLUS | -| SDEY | 1012 | maimai FiNALE | +| Game Code | Version ID | Version Name | +|-----------|------------|-------------------------| +| SBXL | 0 | maimai | +| SBXL | 1 | maimai PLUS | +| SBZF | 2 | maimai GreeN | +| SBZF | 3 | maimai GreeN PLUS | +| SDBM | 4 | maimai ORANGE | +| SDBM | 5 | maimai ORANGE PLUS | +| SDCQ | 6 | maimai PiNK | +| SDCQ | 7 | maimai PiNK PLUS | +| SDDK | 8 | maimai MURASAKI | +| SDDK | 9 | maimai MURASAKI PLUS | +| SDDZ | 10 | maimai MILK | +| SDDZ | 11 | maimai MILK PLUS | +| SDEY | 12 | maimai FiNALE | +| SDEZ | 13 | maimai DX | +| SDEZ | 14 | maimai DX PLUS | +| SDEZ | 15 | maimai DX Splash | +| SDEZ | 16 | maimai DX Splash PLUS | +| SDEZ | 17 | maimai DX Universe | +| SDEZ | 18 | maimai DX Universe PLUS | +| SDEZ | 19 | maimai DX Festival | ### Importer diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 7063ee6..2fa3874 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -16,4 +16,4 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, ] -current_schema_version = 6 +current_schema_version = 7 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index a53e416..9f1ffaf 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -140,6 +140,7 @@ class Mai2Base: def handle_user_login_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) + consec = self.data.profile.get_consec_login(data["userId"], self.version) if profile is not None: lastLoginDate = profile["lastLoginDate"] @@ -150,12 +151,32 @@ class Mai2Base: else: loginCt = 0 lastLoginDate = "2017-12-05 07:00:00.0" + + if consec is None or not consec: + consec_ct = 1 + + else: + lastlogindate_ = datetime.strptime(profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f").timestamp() + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp() + yesterday_midnight = today_midnight - 86400 + + if lastlogindate_ < today_midnight: + consec_ct = consec['logins'] + 1 + self.data.profile.add_consec_login(data["userId"], self.version) + + elif lastlogindate_ < yesterday_midnight: + consec_ct = 1 + self.data.profile.reset_consec_login(data["userId"], self.version) + + else: + consec_ct = consec['logins'] + return { "returnCode": 1, "lastLoginDate": lastLoginDate, "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... + "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: @@ -747,3 +768,9 @@ class Mai2Base: "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}], } + + def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + self.logger.debug(data) + + def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: + self.logger.debug(data) \ No newline at end of file diff --git a/titles/mai2/read.py b/titles/mai2/read.py index a7e10de..4ff401d 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -37,7 +37,7 @@ class Mai2Reader(BaseReader): def read(self) -> None: data_dirs = [] - if self.version < Mai2Constants.VER_MAIMAI: + if self.version >= Mai2Constants.VER_MAIMAI_DX: if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -52,7 +52,6 @@ class Mai2Reader(BaseReader): self.read_tickets(f"{dir}/ticket") else: - self.logger.warn("Pre-DX Readers are not yet implemented!") if not os.path.exists(f"{self.bin_dir}/tables"): self.logger.error(f"tables directory not found in {self.bin_dir}") return diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 54bda21..916be20 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -461,6 +461,17 @@ recent_rating = Table( mysql_charset="utf8mb4", ) +consec_logins = Table( + "mai2_profile_consec_logins", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("version", Integer, nullable=False), + Column("logins", Integer), + UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"), + mysql_charset="utf8mb4", +) + class Mai2ProfileData(BaseData): def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True @@ -764,4 +775,43 @@ class Mai2ProfileData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() \ No newline at end of file + return result.fetchone() + + def add_consec_login(self, user_id: int, version: int) -> None: + sql = insert(consec_logins).values( + user=user_id, + version=version, + logins=1 + ) + + conflict = sql.on_duplicate_key_update( + logins=consec_logins.c.logins + 1 + ) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to update consecutive login count for user {user_id} version {version}") + + def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = select(consec_logins).where(and_( + consec_logins.c.user==user_id, + consec_logins.c.version==version, + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = consec_logins.update(and_( + consec_logins.c.user==user_id, + consec_logins.c.version==version, + )).values( + logins=1 + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 6b25d14..181a895 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -362,4 +362,4 @@ class Mai2ScoreData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() From a89247cdd60d0d6add2bd2b773f027b34eee0bd9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 2 Jul 2023 02:33:45 -0400 Subject: [PATCH 294/589] wacca: add note about VIP rewards --- docs/game_specific_info.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 0b522aa..d5d1eff 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -421,6 +421,41 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core python dbutils.py --game SDFE upgrade ``` +### VIP Rewards +Below is a list of VIP rewards. Currently, VIP is not implemented, and thus these are not obtainable. These 23 rewards were distributed once per month for VIP users on the real network. + + Plates: + 211004 リッチ + 211018 特盛えりざべす + 211025 イースター + 211026 特盛りりぃ + 311004 ファンシー + 311005 インカンテーション + 311014 夜明け + 311015 ネイビー + 311016 特盛るーん + + Ring Colors: + 203002 Gold Rushイエロー + 203009 トロピカル + 303005 ネイチャー + + Icons: + 202020 どらみんぐ + 202063 ユニコーン + 202086 ゴリラ + 302014 ローズ + 302015 ファラオ + 302045 肉球 + 302046 WACCA + 302047 WACCA Lily + 302048 WACCA Reverse + + Note Sound Effect: + 205002 テニス + 205008 シャワー + 305003 タンバリンMk-Ⅱ + ## SAO ### SDEW From 432177957a282bc92fc1a7c1ef407a680162ed6f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 2 Jul 2023 02:42:49 -0400 Subject: [PATCH 295/589] pokken: save most profile data --- titles/pokken/base.py | 49 +++++++++++++++++++++++++--- titles/pokken/schema/profile.py | 57 +++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 50bc760..0b849b3 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -8,6 +8,7 @@ from core import CoreConfig from .config import PokkenConfig from .proto import jackal_pb2 from .database import PokkenData +from .const import PokkenConstants class PokkenBase: @@ -301,11 +302,11 @@ class PokkenBase: battle = req.battle_data mon = req.pokemon_data - self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) - self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) - self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + p = self.data.profile.touch_profile(user_id) + if p is None or not p: + self.data.profile.create_profile(user_id) - if req.trainer_name_pending: # we're saving for the first time + if req.trainer_name_pending is not None and req.trainer_name_pending: # we're saving for the first time self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None) for tut_flg in req.tutorial_progress_flag: @@ -328,6 +329,46 @@ class PokkenBase: for reward in req.reward_data: self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) + + self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max) + + self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) + self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) + self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + + self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp) + self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp) + + for x in range(len(battle.play_mode)): + self.data.profile.put_pokemon_battle_result( + user_id, + mon.char_id, + PokkenConstants.BATTLE_TYPE(battle.play_mode[x]), + PokkenConstants.BATTLE_RESULT(battle.result[x]) + ) + + self.data.profile.put_stats( + user_id, + battle.ex_ko_num, + battle.wko_num, + battle.timeup_win_num, + battle.cool_ko_num, + battle.perfect_ko_num, + num_continues + ) + + self.data.profile.put_extra( + user_id, + extra_counter, + evt_reward_get_flg, + total_play_days, + awake_num, + use_support_ct, + beat_num, + aid_skill, + last_evt + ) + return res.SerializeToString() diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 94e15e8..812964d 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -137,6 +137,14 @@ pokemon_data = Table( class PokkenProfileData(BaseData): + def touch_profile(self, user_id: int) -> Optional[int]: + sql = select([profile.c.id]).where(profile.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone()['id'] + def create_profile(self, user_id: int) -> Optional[int]: sql = insert(profile).values(user=user_id) conflict = sql.on_duplicate_key_update(user=user_id) @@ -158,6 +166,33 @@ class PokkenProfileData(BaseData): f"Failed to update pokken profile name for user {user_id}!" ) + def put_extra( + self, + user_id: int, + extra_counter: int, + evt_reward_get_flg: int, + total_play_days: int, + awake_num: int, + use_support_ct: int, + beat_num: int, + aid_skill: int, + last_evt: int + ) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + extra_counter=extra_counter, + event_reward_get_flag=evt_reward_get_flg, + total_play_days=total_play_days, + awake_num=awake_num, + use_support_num=use_support_ct, + beat_num=beat_num, + aid_skill=aid_skill, + last_play_event_id=last_evt + ) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to put extra data for user {user_id}") + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None: sql = update(profile).where(profile.c.user == user_id).values( tutorial_progress_flag=tutorial_flags, @@ -192,9 +227,14 @@ class PokkenProfileData(BaseData): ) def add_profile_points( - self, user_id: int, rank_pts: int, money: int, score_pts: int + self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int ) -> None: - pass + sql = update(profile).where(profile.c.user == user_id).values( + trainer_rank_point = profile.c.trainer_rank_point + rank_pts, + fight_money = profile.c.fight_money + money, + score_point = profile.c.score_point + score_pts, + grade_max_num = grade_max + ) def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) @@ -294,7 +334,18 @@ class PokkenProfileData(BaseData): """ Records profile stats """ - pass + sql = update(profile).where(profile.c.user==user_id).values( + ex_ko_num=profile.c.ex_ko_num + exkos, + wko_num=profile.c.wko_num + wkos, + timeup_win_num=profile.c.timeup_win_num + timeout_wins, + cool_ko_num=profile.c.cool_ko_num + cool_kos, + perfect_ko_num=profile.c.perfect_ko_num + perfects, + continue_num=continues, + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to update stats for user {user_id}") def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None: sql = update(profile).where(profile.c.user==user_id).values( From 84e880e94f66b3e7787790f65ce54cf07b9ad786 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 2 Jul 2023 14:25:24 -0400 Subject: [PATCH 296/589] fixing unanalyzed reward request for SAO --- titles/sao/base.py | 39 +- titles/sao/data/RewardTable.csv | 34548 +++++++++++++++--------------- titles/sao/handlers/base.py | 92 +- titles/sao/index.py | 1 - titles/sao/schema/item.py | 50 +- 5 files changed, 17400 insertions(+), 17330 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index ad03e97..74aa85a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -815,6 +815,8 @@ class SaoBase: ) # Grab the rare loot from the table, match it with the right item and then push to the player profile + json_data = {"data": []} + for r in range(0,req_data.get_rare_drop_data_list_length): rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] @@ -850,9 +852,13 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) + + json_data["data"].append(randomized_unanalyzed_id['CommonRewardId']) # Send response + self.game_data.item.create_end_session(user_id, episode_id, quest_clear_flag, json_data["data"]) + resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -1117,6 +1123,8 @@ class SaoBase: hero_data["skill_slot4_skill_id"], hero_data["skill_slot5_skill_id"] ) + + json_data = {"data": []} # Grab the rare loot from the table, match it with the right item and then push to the player profile for r in range(0,req_data.get_rare_drop_data_list_length): @@ -1154,9 +1162,13 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) + + json_data["data"].append(randomized_unanalyzed_id['CommonRewardId']) # Send response + self.game_data.item.create_end_session(user_id, trial_tower_id, quest_clear_flag, json_data["data"]) + resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -1176,32 +1188,9 @@ class SaoBase: req_data = req_struct.parse(req) user_id = req_data.user_id - with open('titles/sao/data/RewardTable.csv', 'r') as f: - keys_unanalyzed = next(f).strip().split(',') - data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + end_session_data = self.game_data.item.get_end_session(user_id) - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - i = 0 - - # Create a loop to check if the id is a hero or else try 15 times before closing the loop and sending a dummy hero - while not heroList: - if i == 15: - # Return the dummy hero but not save it - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, 102000070) - return resp.make() - - i += 1 - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - - hero_data = self.game_data.item.get_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId']) - - # Avoid having a duplicated card and cause an overwrite - if not hero_data: - self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, randomized_unanalyzed_id['CommonRewardId']) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) return resp.make() def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode diff --git a/titles/sao/data/RewardTable.csv b/titles/sao/data/RewardTable.csv index acce5bd..929ff82 100644 --- a/titles/sao/data/RewardTable.csv +++ b/titles/sao/data/RewardTable.csv @@ -1,17274 +1,17274 @@ -RewardTableId,UnanalyzedLogGradeId,CommonRewardId -142,1,101000000 -143,2,101000001 -144,3,101000002 -145,4,101000008 -146,5,101000004 -147,1,102000000 -148,2,102000001 -149,3,102000002 -150,4,102000003 -151,5,102000004 -152,1,103000000 -153,2,103000001 -154,3,103000002 -155,4,103000003 -156,5,103000004 -157,1,105000000 -158,2,105000001 -159,3,105000002 -160,4,105000003 -161,5,105000004 -162,1,108000000 -163,2,108000001 -164,3,108000002 -165,4,108000003 -166,5,108000004 -167,1,109000000 -168,2,109000001 -169,3,109000002 -170,4,109000003 -171,5,109000004 -172,1,111000000 -173,2,111000001 -174,3,111000002 -175,4,111000003 -176,5,111000004 -177,1,112000000 -178,2,112000001 -179,3,112000002 -180,4,112000003 -181,5,112000004 -182,1,120000000 -183,2,120000001 -184,3,120000002 -185,4,120000003 -186,5,120000004 -187,1,101000010 -188,2,101000030 -189,3,101000040 -190,1,102000010 -191,2,102000030 -192,3,102000070 -193,1,103000010 -194,2,103000040 -195,3,103000070 -196,1,104000010 -197,2,104000030 -198,3,104000070 -199,1,105000010 -200,2,105000040 -201,3,105000070 -202,1,106000010 -203,2,106000030 -204,3,106000070 -205,1,107000010 -206,2,107000040 -207,3,107000070 -208,1,108000010 -209,2,108000030 -210,3,108000050 -212,1,101000000 -213,2,101000001 -214,3,101000002 -215,4,101000008 -216,5,101000004 -218,1,101000000 -219,2,101000001 -220,3,101000002 -221,4,101000008 -222,5,101000004 -223,1,102000000 -224,2,102000001 -225,3,102000002 -226,4,102000003 -227,5,102000004 -228,1,103000000 -229,2,103000001 -230,3,103000002 -231,4,103000003 -232,5,103000004 -233,1,105000000 -234,2,105000001 -235,3,105000002 -236,4,105000003 -237,5,105000004 -238,1,108000000 -239,2,108000001 -240,3,108000002 -241,4,108000003 -242,5,108000004 -243,1,109000000 -244,2,109000001 -245,3,109000002 -246,4,109000003 -247,5,109000004 -248,1,111000000 -249,2,111000001 -250,3,111000002 -251,4,111000003 -252,5,111000004 -253,1,112000000 -254,2,112000001 -255,3,112000002 -256,4,112000003 -257,5,112000004 -258,1,120000000 -259,2,120000001 -260,3,120000002 -261,4,120000003 -262,5,120000004 -263,1,101000010 -264,2,101000020 -265,2,101000030 -266,3,101000040 -267,3,101000050 -268,3,101000060 -269,3,101000070 -270,3,101000080 -271,1,102000010 -272,2,102000020 -273,2,102000030 -274,2,102000040 -275,3,102000050 -276,3,102000060 -277,3,102000070 -278,3,102000080 -279,1,103000010 -280,2,103000020 -281,2,103000030 -282,2,103000040 -283,3,103000050 -284,3,103000060 -285,3,103000070 -286,3,103000080 -287,1,104000010 -288,2,104000020 -289,2,104000030 -290,2,104000040 -291,3,104000050 -292,3,104000060 -293,3,104000070 -294,3,104000080 -295,1,105000010 -296,2,105000020 -297,2,105000030 -298,2,105000040 -299,3,105000050 -300,3,105000060 -301,3,105000070 -302,3,105000080 -303,1,106000010 -304,2,106000020 -305,2,106000030 -306,2,106000040 -307,3,106000050 -308,3,106000060 -309,3,106000070 -310,3,106000080 -311,1,107000010 -312,2,107000020 -313,2,107000030 -314,2,107000040 -315,3,107000050 -316,3,107000060 -317,3,107000070 -318,3,107000080 -319,1,108000010 -320,2,108000020 -321,2,108000030 -322,2,108000040 -323,3,108000050 -324,3,108000060 -325,3,108000070 -326,3,108000080 -327,2,180000 -329,1,101000000 -330,2,101000001 -331,3,101000002 -332,4,101000008 -333,5,101000004 -334,1,102000000 -335,2,102000001 -336,3,102000002 -337,4,102000003 -338,5,102000004 -339,1,103000000 -340,2,103000001 -341,3,103000002 -342,4,103000003 -343,5,103000004 -344,1,105000000 -345,2,105000001 -346,3,105000002 -347,4,105000003 -348,5,105000004 -349,1,108000000 -350,2,108000001 -351,3,108000002 -352,4,108000003 -353,5,108000004 -354,1,109000000 -355,2,109000001 -356,3,109000002 -357,4,109000003 -358,5,109000004 -359,1,111000000 -360,2,111000001 -361,3,111000002 -362,4,111000003 -363,5,111000004 -364,1,112000000 -365,2,112000001 -366,3,112000002 -367,4,112000003 -368,5,112000004 -369,1,120000000 -370,2,120000001 -371,3,120000002 -372,4,120000003 -373,5,120000004 -374,1,101000010 -375,2,101000020 -376,2,101000030 -377,3,101000040 -378,3,101000050 -379,3,101000060 -380,3,101000070 -381,3,101000080 -382,1,102000010 -383,2,102000020 -384,2,102000030 -385,2,102000040 -386,3,102000050 -387,3,102000060 -388,3,102000070 -389,3,102000080 -390,1,103000010 -391,2,103000020 -392,2,103000030 -393,2,103000040 -394,3,103000050 -395,3,103000060 -396,3,103000070 -397,3,103000080 -398,1,104000010 -399,2,104000020 -400,2,104000030 -401,2,104000040 -402,3,104000050 -403,3,104000060 -404,3,104000070 -405,3,104000080 -406,1,105000010 -407,2,105000020 -408,2,105000030 -409,2,105000040 -410,3,105000050 -411,3,105000060 -412,3,105000070 -413,3,105000080 -414,1,106000010 -415,2,106000020 -416,2,106000030 -417,2,106000040 -418,3,106000050 -419,3,106000060 -420,3,106000070 -421,3,106000080 -422,1,107000010 -423,2,107000020 -424,2,107000030 -425,2,107000040 -426,3,107000050 -427,3,107000060 -428,3,107000070 -429,3,107000080 -430,1,108000010 -431,2,108000020 -432,2,108000030 -433,2,108000040 -434,3,108000050 -435,3,108000060 -436,3,108000070 -437,3,108000080 -438,2,180000 -440,1,101000000 -441,2,101000001 -442,3,101000002 -443,4,101000008 -444,5,101000004 -446,1,102000000 -447,2,102000001 -448,3,102000002 -449,4,102000003 -450,5,102000004 -451,1,112000000 -452,2,112000001 -453,3,112000002 -454,4,112000003 -455,5,112000004 -457,1,101000000 -458,2,101000001 -459,3,101000002 -460,4,101000008 -461,5,101000004 -462,1,120000000 -463,2,120000001 -464,3,120000002 -465,4,120000003 -466,5,120000004 -468,1,111000000 -469,2,111000001 -470,3,111000002 -471,4,111000003 -472,5,111000004 -473,1,120000000 -474,2,120000001 -475,3,120000002 -476,4,120000003 -477,5,120000004 -479,1,109000000 -480,2,109000001 -481,3,109000002 -482,4,109000003 -483,5,109000004 -484,1,112000000 -485,2,112000001 -486,3,112000002 -487,4,112000003 -488,5,112000004 -490,1,103000000 -491,2,103000001 -492,3,103000002 -493,4,103000003 -494,5,103000004 -495,1,112000000 -496,2,112000001 -497,3,112000002 -498,4,112000003 -499,5,112000004 -501,1,105000000 -502,2,105000001 -503,3,105000002 -504,4,105000003 -505,5,105000004 -506,1,120000000 -507,2,120000001 -508,3,120000002 -509,4,120000003 -510,5,120000004 -512,1,108000000 -513,2,108000001 -514,3,108000002 -515,4,108000003 -516,5,108000004 -517,1,120000000 -518,2,120000001 -519,3,120000002 -520,4,120000003 -521,5,120000004 -523,1,101000010 -524,1,103000010 -525,2,103000030 -526,2,103000040 -527,2,107000020 -528,2,101000030 -529,3,101000050 -530,3,101000060 -531,3,101000080 -532,3,103000060 -533,3,103000070 -534,3,103000080 -535,3,101000040 -536,1,101000000 -537,2,101000001 -538,3,101000002 -539,4,101000008 -540,5,101000004 -541,1,112000000 -542,2,112000001 -543,3,112000002 -544,4,112000003 -545,5,112000004 -546,2,101000005 -547,2,112000005 -548,3,170000 -549,4,170001 -551,1,102000010 -552,2,102000030 -553,2,102000040 -554,2,104000020 -555,3,102000060 -556,3,102000070 -557,3,102000080 -558,3,103000050 -559,3,105000050 -560,1,102000000 -561,2,102000001 -562,3,102000002 -563,4,102000003 -564,5,102000004 -565,1,112000000 -566,2,112000001 -567,3,112000002 -568,4,112000003 -569,5,112000004 -570,3,170000 -571,4,170001 -573,1,106000010 -574,2,106000030 -575,2,106000040 -576,2,105000020 -577,3,106000060 -578,3,106000070 -579,3,106000080 -580,3,101000070 -581,1,103000000 -582,2,103000001 -583,3,103000002 -584,4,103000003 -585,5,103000004 -586,1,112000000 -587,2,112000001 -588,3,112000002 -589,4,112000003 -590,5,112000004 -591,3,170000 -592,4,170001 -594,1,104000010 -595,2,104000030 -596,2,104000040 -597,2,108000020 -598,3,104000060 -599,3,104000070 -600,3,104000080 -601,3,102000050 -602,1,111000000 -603,2,111000001 -604,3,111000002 -605,4,111000003 -606,5,111000004 -607,1,120000000 -608,2,120000001 -609,3,120000002 -610,4,120000003 -611,5,120000004 -612,1,110020 -613,1,110030 -614,1,110040 -615,1,110050 -616,3,110060 -617,3,110070 -618,3,110080 -619,3,110090 -620,3,110100 -621,3,110110 -622,3,110120 -623,3,110130 -624,3,110140 -625,3,110150 -626,3,110160 -627,3,110170 -628,3,110180 -629,3,110190 -630,3,110200 -631,3,110210 -632,3,110220 -633,3,110230 -634,3,110240 -635,3,110250 -636,3,110260 -637,3,110270 -639,1,107000010 -640,2,107000030 -641,2,107000040 -642,2,101000020 -643,3,107000060 -644,3,107000070 -645,3,107000080 -646,3,108000050 -647,1,105000000 -648,2,105000001 -649,3,105000002 -650,4,105000003 -651,5,105000004 -652,1,120000000 -653,2,120000001 -654,3,120000002 -655,4,120000003 -656,5,120000004 -657,3,180000 -658,4,180001 -660,1,105000010 -661,2,105000030 -662,2,105000040 -663,2,102000020 -664,2,103000020 -665,3,105000060 -666,3,105000070 -667,3,105000080 -668,3,104000050 -669,3,106000050 -670,1,109000000 -671,2,109000001 -672,3,109000002 -673,4,109000003 -674,5,109000004 -675,1,112000000 -676,2,112000001 -677,3,112000002 -678,4,112000003 -679,5,112000004 -680,1,120020 -681,1,120030 -682,1,120040 -683,1,120050 -684,3,120240 -685,3,120250 -686,3,120260 -687,3,120270 -688,3,120300 -689,3,120310 -690,3,120320 -691,3,120330 -692,3,120340 -693,3,120350 -694,3,120360 -695,3,120370 -696,3,120380 -697,3,120390 -698,3,120400 -699,3,120410 -700,3,120420 -701,3,120430 -702,3,120450 -703,3,120460 -705,1,108000010 -706,2,108000030 -707,2,108000040 -708,2,106000020 -709,3,108000060 -710,3,108000070 -711,3,108000080 -712,3,107000050 -713,1,108000000 -714,2,108000001 -715,3,108000002 -716,4,108000003 -717,5,108000004 -718,1,120000000 -719,2,120000001 -720,3,120000002 -721,4,120000003 -722,5,120000004 -723,1,130020 -724,1,130030 -725,1,130040 -726,1,130050 -727,3,130060 -728,3,130070 -729,3,130080 -730,3,130090 -731,3,130100 -732,3,130110 -733,3,130120 -734,3,130130 -735,3,130140 -736,3,130150 -737,3,130160 -738,3,130170 -739,3,130180 -740,3,130190 -741,3,130200 -742,3,130420 -743,3,130510 -744,3,130520 -745,3,130530 -746,3,130540 -748,1,105000010 -749,2,105000030 -750,2,105000040 -751,2,102000020 -752,2,103000020 -753,3,105000060 -754,3,105000070 -755,3,105000080 -756,3,104000050 -757,3,106000050 -758,1,109000000 -759,2,109000001 -760,3,109000002 -761,4,109000003 -762,5,109000004 -763,1,112000000 -764,2,112000001 -765,3,112000002 -766,4,112000003 -767,5,112000004 -768,3,170000 -769,4,170001 -771,1,104000010 -772,2,104000030 -773,2,104000040 -774,2,108000020 -775,3,104000060 -776,3,104000070 -777,3,104000080 -778,3,102000050 -779,1,111000000 -780,2,111000001 -781,3,111000002 -782,4,111000003 -783,5,111000004 -784,1,120000000 -785,2,120000001 -786,3,120000002 -787,4,120000003 -788,5,120000004 -789,1,110020 -790,1,110030 -791,1,110040 -792,1,110050 -793,3,110060 -794,3,110070 -795,3,110080 -796,3,110090 -797,3,110100 -798,3,110110 -799,3,110120 -800,3,110130 -801,3,110140 -802,3,110150 -803,3,110160 -804,3,110170 -805,3,110180 -806,3,110190 -807,3,110200 -808,3,110210 -809,3,110220 -810,3,110230 -811,3,110240 -812,3,110250 -813,3,110260 -814,3,110270 -816,1,102000010 -817,2,102000030 -818,2,102000040 -819,2,104000020 -820,3,102000060 -821,3,102000070 -822,3,102000080 -823,3,103000050 -824,3,105000050 -825,1,102000000 -826,2,102000001 -827,3,102000002 -828,4,102000003 -829,5,102000004 -830,1,112000000 -831,2,112000001 -832,3,112000002 -833,4,112000003 -834,5,112000004 -835,3,170001 -836,4,170002 -838,1,102000010 -839,2,102000030 -840,2,102000040 -841,2,104000020 -842,3,102000060 -843,3,102000070 -844,3,102000080 -845,3,103000050 -846,3,105000050 -847,1,102000000 -848,2,102000001 -849,3,102000002 -850,4,102000003 -851,5,102000004 -852,1,112000000 -853,2,112000001 -854,3,112000002 -855,4,112000003 -856,5,112000004 -857,1,110020 -858,1,110030 -859,1,110040 -860,1,110050 -861,2,110021 -862,2,110031 -863,2,110041 -864,2,110051 -865,3,110060 -866,3,110070 -867,3,110080 -868,3,110090 -869,3,110100 -870,3,110110 -871,3,110120 -872,3,110130 -873,3,110140 -874,3,110150 -875,3,110160 -876,3,110170 -877,3,110180 -878,3,110190 -879,3,110200 -880,3,110210 -881,3,110220 -882,3,110230 -883,3,110240 -884,3,110250 -885,3,110260 -886,3,110270 -887,4,140000 -889,1,101000010 -890,1,103000010 -891,2,103000030 -892,2,103000040 -893,2,107000020 -894,2,101000030 -895,3,101000050 -896,3,101000060 -897,3,101000080 -898,3,103000060 -899,3,103000070 -900,3,103000080 -901,3,101000040 -902,1,101000000 -903,2,101000001 -904,3,101000002 -905,4,101000008 -906,5,101000004 -907,1,112000000 -908,2,112000001 -909,3,112000002 -910,4,112000003 -911,5,112000004 -912,1,120020 -913,1,120030 -914,1,120040 -915,1,120050 -916,2,120021 -917,2,120031 -918,2,120041 -919,2,120051 -920,3,120240 -921,3,120250 -922,3,120260 -923,3,120270 -924,3,120300 -925,3,120310 -926,3,120320 -927,3,120330 -928,3,120340 -929,3,120350 -930,3,120360 -931,3,120370 -932,3,120380 -933,3,120390 -934,3,120400 -935,3,120410 -936,3,120420 -937,3,120430 -938,3,120450 -939,3,120460 -940,4,140000 -942,1,106000010 -943,2,106000030 -944,2,106000040 -945,2,105000020 -946,3,106000060 -947,3,106000070 -948,3,106000080 -949,3,101000070 -950,1,103000000 -951,2,103000001 -952,3,103000002 -953,4,103000003 -954,5,103000004 -955,1,112000000 -956,2,112000001 -957,3,112000002 -958,4,112000003 -959,5,112000004 -960,3,180001 -961,4,180002 -963,1,101000010 -964,1,103000010 -965,2,103000030 -966,2,103000040 -967,2,107000020 -968,2,101000030 -969,3,101000050 -970,3,101000060 -971,3,101000080 -972,3,103000060 -973,3,103000070 -974,3,103000080 -975,3,101000040 -976,1,101000000 -977,2,101000001 -978,3,101000002 -979,4,101000008 -980,5,101000004 -981,1,120000000 -982,2,120000001 -983,3,120000002 -984,4,120000003 -985,5,120000004 -986,3,170001 -987,4,170002 -989,1,105000010 -990,2,105000030 -991,2,105000040 -992,2,102000020 -993,2,103000020 -994,3,105000060 -995,3,105000070 -996,3,105000080 -997,3,104000050 -998,3,106000050 -999,1,109000000 -1000,2,109000001 -1001,3,109000002 -1002,4,109000003 -1003,5,109000004 -1004,1,112000000 -1005,2,112000001 -1006,3,112000002 -1007,4,112000003 -1008,5,112000004 -1009,1,130020 -1010,1,130030 -1011,1,130040 -1012,1,130050 -1013,2,130021 -1014,2,130031 -1015,2,130041 -1016,2,130051 -1017,3,130060 -1018,3,130070 -1019,3,130080 -1020,3,130090 -1021,3,130100 -1022,3,130110 -1023,3,130120 -1024,3,130130 -1025,3,130140 -1026,3,130150 -1027,3,130160 -1028,3,130170 -1029,3,130180 -1030,3,130190 -1031,3,130200 -1032,3,130420 -1033,3,130510 -1034,3,130520 -1035,3,130530 -1036,3,130540 -1037,4,140000 -1039,1,107000010 -1040,2,107000030 -1041,2,107000040 -1042,2,101000020 -1043,3,107000060 -1044,3,107000070 -1045,3,107000080 -1046,3,108000050 -1047,1,105000000 -1048,2,105000001 -1049,3,105000002 -1050,4,105000003 -1051,5,105000004 -1052,1,120000000 -1053,2,120000001 -1054,3,120000002 -1055,4,120000003 -1056,5,120000004 -1057,1,120020 -1058,1,120030 -1059,1,120040 -1060,1,120050 -1061,2,120021 -1062,2,120031 -1063,2,120041 -1064,2,120051 -1065,3,120240 -1066,3,120250 -1067,3,120260 -1068,3,120270 -1069,3,120300 -1070,3,120310 -1071,3,120320 -1072,3,120330 -1073,3,120340 -1074,3,120350 -1075,3,120360 -1076,3,120370 -1077,3,120380 -1078,3,120390 -1079,3,120400 -1080,3,120410 -1081,3,120420 -1082,3,120430 -1083,3,120450 -1084,3,120460 -1085,4,140000 -1087,1,108000010 -1088,2,108000030 -1089,2,108000040 -1090,2,106000020 -1091,3,108000060 -1092,3,108000070 -1093,3,108000080 -1094,3,107000050 -1095,1,108000000 -1096,2,108000001 -1097,3,108000002 -1098,4,108000003 -1099,5,108000004 -1100,1,120000000 -1101,2,120000001 -1102,3,120000002 -1103,4,120000003 -1104,5,120000004 -1105,3,170001 -1106,4,170002 -1108,1,101000010 -1109,1,103000010 -1110,2,103000030 -1111,2,103000040 -1112,2,107000020 -1113,2,101000030 -1114,3,101000050 -1115,3,101000060 -1116,3,101000080 -1117,3,103000060 -1118,3,103000070 -1119,3,103000080 -1120,3,101000040 -1121,1,101000000 -1122,2,101000001 -1123,3,101000002 -1124,4,101000008 -1125,5,101000004 -1126,1,112000000 -1127,2,112000001 -1128,3,112000002 -1129,4,112000003 -1130,5,112000004 -1131,2,101000005 -1132,2,112000005 -1133,3,170001 -1134,4,170002 -1136,1,102000010 -1137,2,102000030 -1138,2,102000040 -1139,2,104000020 -1140,3,102000060 -1141,3,102000070 -1142,3,102000080 -1143,3,103000050 -1144,3,105000050 -1145,1,102000000 -1146,2,102000001 -1147,3,102000002 -1148,4,102000003 -1149,5,102000004 -1150,1,112000000 -1151,2,112000001 -1152,3,112000002 -1153,4,112000003 -1154,5,112000004 -1155,1,120020 -1156,1,120030 -1157,1,120040 -1158,1,120050 -1159,2,120021 -1160,2,120031 -1161,2,120041 -1162,2,120051 -1163,3,120240 -1164,3,120250 -1165,3,120260 -1166,3,120270 -1167,3,120300 -1168,3,120310 -1169,3,120320 -1170,3,120330 -1171,3,120340 -1172,3,120350 -1173,3,120360 -1174,3,120370 -1175,3,120380 -1176,3,120390 -1177,3,120400 -1178,3,120410 -1179,3,120420 -1180,3,120430 -1181,3,120450 -1182,3,120460 -1183,4,140000 -1185,2,104000030 -1186,2,104000040 -1187,2,108000020 -1188,3,104000060 -1189,3,104000070 -1190,3,104000080 -1191,3,102000050 -1192,4,104000100 -1193,4,104000110 -1194,4,107000090 -1195,1,111000000 -1196,2,111000001 -1197,3,111000002 -1198,4,111000003 -1199,5,111000004 -1200,1,120000000 -1201,2,120000001 -1202,3,120000002 -1203,4,120000003 -1204,5,120000004 -1205,3,170002 -1206,4,170003 -1208,2,104000030 -1209,2,104000040 -1210,2,108000020 -1211,3,104000060 -1212,3,104000070 -1213,3,104000080 -1214,3,102000050 -1215,4,104000100 -1216,4,104000110 -1217,4,107000090 -1218,1,111000000 -1219,2,111000001 -1220,3,111000002 -1221,4,111000003 -1222,5,111000004 -1223,1,120000000 -1224,2,120000001 -1225,3,120000002 -1226,4,120000003 -1227,5,120000004 -1228,1,110020 -1229,1,110030 -1230,1,110040 -1231,1,110050 -1232,2,110021 -1233,2,110031 -1234,2,110041 -1235,2,110051 -1236,3,110022 -1237,3,110032 -1238,3,110042 -1239,3,110052 -1240,3,110060 -1241,3,110070 -1242,3,110080 -1243,3,110090 -1244,3,110100 -1245,3,110110 -1246,3,110120 -1247,3,110130 -1248,3,110140 -1249,3,110150 -1250,3,110160 -1251,3,110170 -1252,3,110180 -1253,3,110190 -1254,3,110200 -1255,3,110210 -1256,3,110220 -1257,3,110230 -1258,3,110240 -1259,3,110250 -1260,3,110260 -1261,3,110270 -1262,4,140000 -1264,2,105000030 -1265,2,105000040 -1266,2,102000020 -1267,2,103000020 -1268,3,105000060 -1269,3,105000070 -1270,3,105000080 -1271,3,104000050 -1272,3,106000050 -1273,4,105000100 -1274,4,105000110 -1275,4,108000090 -1276,1,109000000 -1277,2,109000001 -1278,3,109000002 -1279,4,109000003 -1280,5,109000004 -1281,1,112000000 -1282,2,112000001 -1283,3,112000002 -1284,4,112000003 -1285,5,112000004 -1286,3,170002 -1287,4,170003 -1289,2,106000030 -1290,2,106000040 -1291,2,105000020 -1292,3,106000060 -1293,3,106000070 -1294,3,106000080 -1295,3,101000070 -1296,4,106000100 -1297,4,106000110 -1298,4,104000090 -1299,1,103000000 -1300,2,103000001 -1301,3,103000002 -1302,4,103000003 -1303,5,103000004 -1304,1,112000000 -1305,2,112000001 -1306,3,112000002 -1307,4,112000003 -1308,5,112000004 -1309,1,130020 -1310,1,130030 -1311,1,130040 -1312,1,130050 -1313,2,130021 -1314,2,130031 -1315,2,130041 -1316,2,130051 -1317,3,130022 -1318,3,130032 -1319,3,130042 -1320,3,130052 -1321,3,130060 -1322,3,130070 -1323,3,130080 -1324,3,130090 -1325,3,130100 -1326,3,130110 -1327,3,130120 -1328,3,130130 -1329,3,130140 -1330,3,130150 -1331,3,130160 -1332,3,130170 -1333,3,130180 -1334,3,130190 -1335,3,130200 -1336,3,130420 -1337,3,130510 -1338,3,130520 -1339,3,130530 -1340,3,130540 -1341,4,140000 -1343,2,108000030 -1344,2,108000040 -1345,2,106000020 -1346,3,108000060 -1347,3,108000070 -1348,3,108000080 -1349,3,107000050 -1350,4,108000100 -1351,4,105000090 -1352,1,108000000 -1353,2,108000001 -1354,3,108000002 -1355,4,108000003 -1356,5,108000004 -1357,1,120000000 -1358,2,120000001 -1359,3,120000002 -1360,4,120000003 -1361,5,120000004 -1362,1,120020 -1363,1,120030 -1364,1,120040 -1365,1,120050 -1366,2,120021 -1367,2,120031 -1368,2,120041 -1369,2,120051 -1370,3,120022 -1371,3,120032 -1372,3,120042 -1373,3,120052 -1374,3,120240 -1375,3,120250 -1376,3,120260 -1377,3,120270 -1378,3,120300 -1379,3,120310 -1380,3,120320 -1381,3,120330 -1382,3,120340 -1383,3,120350 -1384,3,120360 -1385,3,120370 -1386,3,120380 -1387,3,120390 -1388,3,120400 -1389,3,120410 -1390,3,120420 -1391,3,120430 -1392,3,120450 -1393,3,120460 -1394,4,140000 -1396,2,103000030 -1397,2,103000040 -1398,2,107000020 -1399,2,101000030 -1400,3,101000050 -1401,3,101000060 -1402,3,101000080 -1403,3,103000060 -1404,3,103000070 -1405,3,103000080 -1406,3,101000040 -1407,4,101000090 -1408,4,102000090 -1409,4,103000100 -1410,4,101000100 -1411,4,101000110 -1412,1,101000000 -1413,2,101000001 -1414,3,101000002 -1415,4,101000008 -1416,5,101000004 -1417,1,120000000 -1418,2,120000001 -1419,3,120000002 -1420,4,120000003 -1421,5,120000004 -1422,3,170002 -1423,4,170003 -1425,2,105000030 -1426,2,105000040 -1427,2,102000020 -1428,2,103000020 -1429,3,105000060 -1430,3,105000070 -1431,3,105000080 -1432,3,104000050 -1433,3,106000050 -1434,4,105000100 -1435,4,105000110 -1436,4,108000090 -1437,1,109000000 -1438,2,109000001 -1439,3,109000002 -1440,4,109000003 -1441,5,109000004 -1442,1,112000000 -1443,2,112000001 -1444,3,112000002 -1445,4,112000003 -1446,5,112000004 -1447,3,180002 -1448,4,180003 -1450,2,107000030 -1451,2,107000040 -1452,2,101000020 -1453,3,107000060 -1454,3,107000070 -1455,3,107000080 -1456,3,108000050 -1457,4,107000100 -1458,4,103000090 -1459,1,105000000 -1460,2,105000001 -1461,3,105000002 -1462,4,105000003 -1463,5,105000004 -1464,1,120000000 -1465,2,120000001 -1466,3,120000002 -1467,4,120000003 -1468,5,120000004 -1469,3,170002 -1470,4,170003 -1472,2,104000030 -1473,2,104000040 -1474,2,108000020 -1475,3,104000060 -1476,3,104000070 -1477,3,104000080 -1478,3,102000050 -1479,4,104000100 -1480,4,104000110 -1481,4,107000090 -1482,1,111000000 -1483,2,111000001 -1484,3,111000002 -1485,4,111000003 -1486,5,111000004 -1487,1,120000000 -1488,2,120000001 -1489,3,120000002 -1490,4,120000003 -1491,5,120000004 -1492,1,110020 -1493,1,110030 -1494,1,110040 -1495,1,110050 -1496,2,110021 -1497,2,110031 -1498,2,110041 -1499,2,110051 -1500,3,110022 -1501,3,110032 -1502,3,110042 -1503,3,110052 -1504,3,110060 -1505,3,110070 -1506,3,110080 -1507,3,110090 -1508,3,110100 -1509,3,110110 -1510,3,110120 -1511,3,110130 -1512,3,110140 -1513,3,110150 -1514,3,110160 -1515,3,110170 -1516,3,110180 -1517,3,110190 -1518,3,110200 -1519,3,110210 -1520,3,110220 -1521,3,110230 -1522,3,110240 -1523,3,110250 -1524,3,110260 -1525,3,110270 -1526,4,140000 -1528,2,108000030 -1529,2,108000040 -1530,2,106000020 -1531,3,108000060 -1532,3,108000070 -1533,3,108000080 -1534,3,107000050 -1535,4,108000100 -1536,4,105000090 -1537,1,108000000 -1538,2,108000001 -1539,3,108000002 -1540,4,108000003 -1541,5,108000004 -1542,1,120000000 -1543,2,120000001 -1544,3,120000002 -1545,4,120000003 -1546,5,120000004 -1547,3,170002 -1548,4,170003 -1550,2,103000030 -1551,2,103000040 -1552,2,107000020 -1553,2,101000030 -1554,3,101000050 -1555,3,101000060 -1556,3,101000080 -1557,3,103000060 -1558,3,103000070 -1559,3,103000080 -1560,3,101000040 -1561,4,101000090 -1562,4,102000090 -1563,4,103000100 -1564,4,101000100 -1565,4,101000110 -1566,1,101000000 -1567,2,101000001 -1568,3,101000002 -1569,4,101000008 -1570,5,101000004 -1571,1,112000000 -1572,2,112000001 -1573,3,112000002 -1574,4,112000003 -1575,5,112000004 -1576,1,120020 -1577,1,120030 -1578,1,120040 -1579,1,120050 -1580,2,120021 -1581,2,120031 -1582,2,120041 -1583,2,120051 -1584,3,120022 -1585,3,120032 -1586,3,120042 -1587,3,120052 -1588,4,120023 -1589,4,120033 -1590,4,120043 -1591,4,120053 -1592,3,120240 -1593,3,120250 -1594,3,120260 -1595,3,120270 -1596,3,120300 -1597,3,120310 -1598,3,120320 -1599,3,120330 -1600,3,120340 -1601,3,120350 -1602,3,120360 -1603,3,120370 -1604,3,120380 -1605,3,120390 -1606,3,120400 -1607,3,120410 -1608,3,120420 -1609,3,120430 -1610,3,120450 -1611,3,120460 -1612,4,140000 -1613,4,150010 -1614,4,150020 -1615,4,150030 -1616,4,150040 -1618,2,102000030 -1619,2,102000040 -1620,2,104000020 -1621,3,102000060 -1622,3,102000070 -1623,3,102000080 -1624,3,103000050 -1625,3,105000050 -1626,4,102000100 -1627,4,102000110 -1628,4,106000090 -1629,1,102000000 -1630,2,102000001 -1631,3,102000002 -1632,4,102000003 -1633,5,102000004 -1634,1,112000000 -1635,2,112000001 -1636,3,112000002 -1637,4,112000003 -1638,5,112000004 -1639,1,110020 -1640,1,110030 -1641,1,110040 -1642,1,110050 -1643,2,110021 -1644,2,110031 -1645,2,110041 -1646,2,110051 -1647,3,110022 -1648,3,110032 -1649,3,110042 -1650,3,110052 -1651,4,110023 -1652,4,110033 -1653,4,110043 -1654,4,110053 -1655,3,110060 -1656,3,110070 -1657,3,110080 -1658,3,110090 -1659,3,110100 -1660,3,110110 -1661,3,110120 -1662,3,110130 -1663,3,110140 -1664,3,110150 -1665,3,110160 -1666,3,110170 -1667,3,110180 -1668,3,110190 -1669,3,110200 -1670,3,110210 -1671,3,110220 -1672,3,110230 -1673,3,110240 -1674,3,110250 -1675,3,110260 -1676,3,110270 -1677,4,140000 -1678,4,150010 -1679,4,150020 -1680,4,150030 -1681,4,150040 -1683,2,106000030 -1684,2,106000040 -1685,2,105000020 -1686,3,106000060 -1687,3,106000070 -1688,3,106000080 -1689,3,101000070 -1690,4,106000100 -1691,4,106000110 -1692,4,104000090 -1693,1,103000000 -1694,2,103000001 -1695,3,103000002 -1696,4,103000003 -1697,5,103000004 -1698,1,112000000 -1699,2,112000001 -1700,3,112000002 -1701,4,112000003 -1702,5,112000004 -1703,1,120020 -1704,1,120030 -1705,1,120040 -1706,1,120050 -1707,2,120021 -1708,2,120031 -1709,2,120041 -1710,2,120051 -1711,3,120022 -1712,3,120032 -1713,3,120042 -1714,3,120052 -1715,4,120023 -1716,4,120033 -1717,4,120043 -1718,4,120053 -1719,3,120240 -1720,3,120250 -1721,3,120260 -1722,3,120270 -1723,3,120300 -1724,3,120310 -1725,3,120320 -1726,3,120330 -1727,3,120340 -1728,3,120350 -1729,3,120360 -1730,3,120370 -1731,3,120380 -1732,3,120390 -1733,3,120400 -1734,3,120410 -1735,3,120420 -1736,3,120430 -1737,3,120450 -1738,3,120460 -1739,4,140000 -1740,4,150010 -1741,4,150020 -1742,4,150030 -1743,4,150040 -1745,2,103000030 -1746,2,103000040 -1747,2,107000020 -1748,2,101000030 -1749,3,101000050 -1750,3,101000060 -1751,3,101000080 -1752,3,103000060 -1753,3,103000070 -1754,3,103000080 -1755,3,101000040 -1756,4,101000090 -1757,4,102000090 -1758,4,103000100 -1759,4,101000100 -1760,4,101000110 -1761,1,101000000 -1762,2,101000001 -1763,3,101000002 -1764,4,101000008 -1765,5,101000004 -1766,1,120000000 -1767,2,120000001 -1768,3,120000002 -1769,4,120000003 -1770,5,120000004 -1771,3,170003 -1772,4,170004 -1774,2,107000030 -1775,2,107000040 -1776,2,101000020 -1777,3,107000060 -1778,3,107000070 -1779,3,107000080 -1780,3,108000050 -1781,4,107000100 -1782,4,103000090 -1783,1,105000000 -1784,2,105000001 -1785,3,105000002 -1786,4,105000003 -1787,5,105000004 -1788,1,120000000 -1789,2,120000001 -1790,3,120000002 -1791,4,120000003 -1792,5,120000004 -1793,1,130020 -1794,1,130030 -1795,1,130040 -1796,1,130050 -1797,2,130021 -1798,2,130031 -1799,2,130041 -1800,2,130051 -1801,3,130022 -1802,3,130032 -1803,3,130042 -1804,3,130052 -1805,4,130023 -1806,4,130033 -1807,4,130043 -1808,4,130053 -1809,3,130060 -1810,3,130070 -1811,3,130080 -1812,3,130090 -1813,3,130100 -1814,3,130110 -1815,3,130120 -1816,3,130130 -1817,3,130140 -1818,3,130150 -1819,3,130160 -1820,3,130170 -1821,3,130180 -1822,3,130190 -1823,3,130200 -1824,3,130420 -1825,3,130510 -1826,3,130520 -1827,3,130530 -1828,3,130540 -1829,4,140000 -1830,4,150010 -1831,4,150020 -1832,4,150030 -1833,4,150040 -1835,2,102000030 -1836,2,102000040 -1837,2,104000020 -1838,3,102000060 -1839,3,102000070 -1840,3,102000080 -1841,3,103000050 -1842,3,105000050 -1843,4,102000100 -1844,4,102000110 -1845,4,106000090 -1846,1,102000000 -1847,2,102000001 -1848,3,102000002 -1849,4,102000003 -1850,5,102000004 -1851,1,112000000 -1852,2,112000001 -1853,3,112000002 -1854,4,112000003 -1855,5,112000004 -1856,1,120020 -1857,1,120030 -1858,1,120040 -1859,1,120050 -1860,2,120021 -1861,2,120031 -1862,2,120041 -1863,2,120051 -1864,3,120022 -1865,3,120032 -1866,3,120042 -1867,3,120052 -1868,4,120023 -1869,4,120033 -1870,4,120043 -1871,4,120053 -1872,3,120240 -1873,3,120250 -1874,3,120260 -1875,3,120270 -1876,3,120300 -1877,3,120310 -1878,3,120320 -1879,3,120330 -1880,3,120340 -1881,3,120350 -1882,3,120360 -1883,3,120370 -1884,3,120380 -1885,3,120390 -1886,3,120400 -1887,3,120410 -1888,3,120420 -1889,3,120430 -1890,3,120450 -1891,3,120460 -1892,4,140000 -1893,4,150010 -1894,4,150020 -1895,4,150030 -1896,4,150040 -1898,2,108000030 -1899,2,108000040 -1900,2,106000020 -1901,3,108000060 -1902,3,108000070 -1903,3,108000080 -1904,3,107000050 -1905,4,108000100 -1906,4,105000090 -1907,1,108000000 -1908,2,108000001 -1909,3,108000002 -1910,4,108000003 -1911,5,108000004 -1912,1,120000000 -1913,2,120000001 -1914,3,120000002 -1915,4,120000003 -1916,5,120000004 -1917,3,170003 -1918,4,170004 -1920,2,103000030 -1921,2,103000040 -1922,2,107000020 -1923,2,101000030 -1924,3,101000050 -1925,3,101000060 -1926,3,101000080 -1927,3,103000060 -1928,3,103000070 -1929,3,103000080 -1930,3,101000040 -1931,4,101000090 -1932,4,102000090 -1933,4,103000100 -1934,4,101000100 -1935,4,101000110 -1936,1,101000000 -1937,2,101000001 -1938,3,101000002 -1939,4,101000008 -1940,5,101000004 -1941,1,112000000 -1942,2,112000001 -1943,3,112000002 -1944,4,112000003 -1945,5,112000004 -1946,3,170003 -1947,4,170004 -1949,2,105000030 -1950,2,105000040 -1951,2,102000020 -1952,2,103000020 -1953,3,105000060 -1954,3,105000070 -1955,3,105000080 -1956,3,104000050 -1957,3,106000050 -1958,4,105000100 -1959,4,105000110 -1960,4,108000090 -1961,1,109000000 -1962,2,109000001 -1963,3,109000002 -1964,4,109000003 -1965,5,109000004 -1966,1,112000000 -1967,2,112000001 -1968,3,112000002 -1969,4,112000003 -1970,5,112000004 -1971,3,180003 -1972,4,180004 -1974,2,104000030 -1975,2,104000040 -1976,2,108000020 -1977,3,104000060 -1978,3,104000070 -1979,3,104000080 -1980,3,102000050 -1981,4,104000100 -1982,4,104000110 -1983,4,107000090 -1984,1,111000000 -1985,2,111000001 -1986,3,111000002 -1987,4,111000003 -1988,5,111000004 -1989,1,120000000 -1990,2,120000001 -1991,3,120000002 -1992,4,120000003 -1993,5,120000004 -1994,1,110020 -1995,1,110030 -1996,1,110040 -1997,1,110050 -1998,2,110021 -1999,2,110031 -2000,2,110041 -2001,2,110051 -2002,3,110022 -2003,3,110032 -2004,3,110042 -2005,3,110052 -2006,4,110023 -2007,4,110033 -2008,4,110043 -2009,4,110053 -2010,3,110060 -2011,3,110070 -2012,3,110080 -2013,3,110090 -2014,3,110100 -2015,3,110110 -2016,3,110120 -2017,3,110130 -2018,3,110140 -2019,3,110150 -2020,3,110160 -2021,3,110170 -2022,3,110180 -2023,3,110190 -2024,3,110200 -2025,3,110210 -2026,3,110220 -2027,3,110230 -2028,3,110240 -2029,3,110250 -2030,3,110260 -2031,3,110270 -2032,4,140000 -2033,4,150010 -2034,4,150020 -2035,4,150030 -2036,4,150040 -2038,3,105000060 -2039,3,105000070 -2040,3,105000080 -2041,3,104000050 -2042,3,106000050 -2043,4,105000100 -2044,4,105000110 -2045,4,108000090 -2046,5,105000120 -2047,1,109000000 -2048,2,109000001 -2049,3,109000002 -2050,4,109000003 -2051,5,109000004 -2052,1,112000000 -2053,2,112000001 -2054,3,112000002 -2055,4,112000003 -2056,5,112000004 -2057,3,170004 -2059,3,101000050 -2060,3,101000060 -2061,3,101000080 -2062,3,103000060 -2063,3,103000070 -2064,3,103000080 -2065,3,101000040 -2066,4,101000090 -2067,4,102000090 -2068,4,103000100 -2069,4,101000100 -2070,4,101000110 -2071,5,101000120 -2072,5,103000120 -2073,1,101000000 -2074,2,101000001 -2075,3,101000002 -2076,4,101000008 -2077,5,101000004 -2078,1,120000000 -2079,2,120000001 -2080,3,120000002 -2081,4,120000003 -2082,5,120000004 -2083,3,170004 -2085,3,107000060 -2086,3,107000070 -2087,3,107000080 -2088,3,108000050 -2089,4,107000100 -2090,4,103000090 -2091,5,107000110 -2092,1,105000000 -2093,2,105000001 -2094,3,105000002 -2095,4,105000003 -2096,5,105000004 -2097,1,120000000 -2098,2,120000001 -2099,3,120000002 -2100,4,120000003 -2101,5,120000004 -2102,3,170004 -2104,3,101000050 -2105,3,101000060 -2106,3,101000080 -2107,3,103000060 -2108,3,103000070 -2109,3,103000080 -2110,3,101000040 -2111,4,101000090 -2112,4,102000090 -2113,4,103000100 -2114,4,101000100 -2115,4,101000110 -2116,5,101000120 -2117,5,103000120 -2118,1,101000000 -2119,2,101000001 -2120,3,101000002 -2121,4,101000008 -2122,5,101000004 -2123,1,112000000 -2124,2,112000001 -2125,3,112000002 -2126,4,112000003 -2127,5,112000004 -2128,1,130020 -2129,1,130030 -2130,1,130040 -2131,1,130050 -2132,2,130021 -2133,2,130031 -2134,2,130041 -2135,2,130051 -2136,3,130022 -2137,3,130032 -2138,3,130042 -2139,3,130052 -2140,4,130023 -2141,4,130033 -2142,4,130043 -2143,4,130053 -2144,5,130024 -2145,5,130034 -2146,5,130044 -2147,5,130054 -2148,3,130060 -2149,3,130070 -2150,3,130080 -2151,3,130090 -2152,3,130100 -2153,3,130110 -2154,3,130120 -2155,3,130130 -2156,3,130140 -2157,3,130150 -2158,3,130160 -2159,3,130170 -2160,3,130180 -2161,3,130190 -2162,3,130200 -2163,3,130420 -2164,3,130510 -2165,3,130520 -2166,3,130530 -2167,3,130540 -2168,4,140000 -2169,4,150010 -2170,4,150020 -2171,4,150030 -2172,4,150040 -2174,3,102000060 -2175,3,102000070 -2176,3,102000080 -2177,3,103000050 -2178,3,105000050 -2179,4,102000100 -2180,4,102000110 -2181,4,106000090 -2182,5,102000120 -2183,1,102000000 -2184,2,102000001 -2185,3,102000002 -2186,4,102000003 -2187,5,102000004 -2188,1,112000000 -2189,2,112000001 -2190,3,112000002 -2191,4,112000003 -2192,5,112000004 -2193,3,170004 -2195,3,101000050 -2196,3,101000060 -2197,3,101000080 -2198,3,103000060 -2199,3,103000070 -2200,3,103000080 -2201,3,101000040 -2202,4,101000090 -2203,4,102000090 -2204,4,103000100 -2205,4,101000100 -2206,4,101000110 -2207,5,101000120 -2208,5,103000120 -2209,1,101000000 -2210,2,101000001 -2211,3,101000002 -2212,4,101000008 -2213,5,101000004 -2214,1,120000000 -2215,2,120000001 -2216,3,120000002 -2217,4,120000003 -2218,5,120000004 -2219,3,170004 -2221,3,106000060 -2222,3,106000070 -2223,3,106000080 -2224,3,101000070 -2225,4,106000100 -2226,4,106000110 -2227,4,104000090 -2228,5,106000120 -2229,1,103000000 -2230,2,103000001 -2231,3,103000002 -2232,4,103000003 -2233,5,103000004 -2234,1,112000000 -2235,2,112000001 -2236,3,112000002 -2237,4,112000003 -2238,5,112000004 -2239,1,130020 -2240,1,130030 -2241,1,130040 -2242,1,130050 -2243,2,130021 -2244,2,130031 -2245,2,130041 -2246,2,130051 -2247,3,130022 -2248,3,130032 -2249,3,130042 -2250,3,130052 -2251,4,130023 -2252,4,130033 -2253,4,130043 -2254,4,130053 -2255,5,130024 -2256,5,130034 -2257,5,130044 -2258,5,130054 -2259,3,130060 -2260,3,130070 -2261,3,130080 -2262,3,130090 -2263,3,130100 -2264,3,130110 -2265,3,130120 -2266,3,130130 -2267,3,130140 -2268,3,130150 -2269,3,130160 -2270,3,130170 -2271,3,130180 -2272,3,130190 -2273,3,130200 -2274,3,130420 -2275,3,130510 -2276,3,130520 -2277,3,130530 -2278,3,130540 -2279,4,140000 -2280,4,150010 -2281,4,150020 -2282,4,150030 -2283,4,150040 -2285,3,108000060 -2286,3,108000070 -2287,3,108000080 -2288,3,107000050 -2289,4,108000100 -2290,4,105000090 -2291,5,108000110 -2292,1,108000000 -2293,2,108000001 -2294,3,108000002 -2295,4,108000003 -2296,5,108000004 -2297,1,120000000 -2298,2,120000001 -2299,3,120000002 -2300,4,120000003 -2301,5,120000004 -2302,1,120020 -2303,1,120030 -2304,1,120040 -2305,1,120050 -2306,2,120021 -2307,2,120031 -2308,2,120041 -2309,2,120051 -2310,3,120022 -2311,3,120032 -2312,3,120042 -2313,3,120052 -2314,4,120023 -2315,4,120033 -2316,4,120043 -2317,4,120053 -2318,5,120024 -2319,5,120034 -2320,5,120044 -2321,5,120054 -2322,3,120240 -2323,3,120250 -2324,3,120260 -2325,3,120270 -2326,3,120300 -2327,3,120310 -2328,3,120320 -2329,3,120330 -2330,3,120340 -2331,3,120350 -2332,3,120360 -2333,3,120370 -2334,3,120380 -2335,3,120390 -2336,3,120400 -2337,3,120410 -2338,3,120420 -2339,3,120430 -2340,3,120450 -2341,3,120460 -2342,4,140000 -2343,4,150010 -2344,4,150020 -2345,4,150030 -2346,4,150040 -2348,3,104000060 -2349,3,104000070 -2350,3,104000080 -2351,3,102000050 -2352,4,104000100 -2353,4,104000110 -2354,4,107000090 -2355,5,104000120 -2356,1,111000000 -2357,2,111000001 -2358,3,111000002 -2359,4,111000003 -2360,5,111000004 -2361,1,120000000 -2362,2,120000001 -2363,3,120000002 -2364,4,120000003 -2365,5,120000004 -2366,1,110020 -2367,1,110030 -2368,1,110040 -2369,1,110050 -2370,2,110021 -2371,2,110031 -2372,2,110041 -2373,2,110051 -2374,3,110022 -2375,3,110032 -2376,3,110042 -2377,3,110052 -2378,4,110023 -2379,4,110033 -2380,4,110043 -2381,4,110053 -2382,5,110024 -2383,5,110034 -2384,5,110044 -2385,5,110054 -2386,3,110060 -2387,3,110070 -2388,3,110080 -2389,3,110090 -2390,3,110100 -2391,3,110110 -2392,3,110120 -2393,3,110130 -2394,3,110140 -2395,3,110150 -2396,3,110160 -2397,3,110170 -2398,3,110180 -2399,3,110190 -2400,3,110200 -2401,3,110210 -2402,3,110220 -2403,3,110230 -2404,3,110240 -2405,3,110250 -2406,3,110260 -2407,3,110270 -2408,4,140000 -2409,4,150010 -2410,4,150020 -2411,4,150030 -2412,4,150040 -2414,3,105000060 -2415,3,105000070 -2416,3,105000080 -2417,3,104000050 -2418,3,106000050 -2419,4,105000100 -2420,4,105000110 -2421,4,108000090 -2422,5,105000120 -2423,1,109000000 -2424,2,109000001 -2425,3,109000002 -2426,4,109000003 -2427,5,109000004 -2428,1,112000000 -2429,2,112000001 -2430,3,112000002 -2431,4,112000003 -2432,5,112000004 -2433,3,170004 -2435,3,104000060 -2436,3,104000070 -2437,3,104000080 -2438,3,102000050 -2439,4,104000100 -2440,4,104000110 -2441,4,107000090 -2442,5,104000120 -2443,1,111000000 -2444,2,111000001 -2445,3,111000002 -2446,4,111000003 -2447,5,111000004 -2448,1,120000000 -2449,2,120000001 -2450,3,120000002 -2451,4,120000003 -2452,5,120000004 -2453,1,130020 -2454,1,130030 -2455,1,130040 -2456,1,130050 -2457,2,130021 -2458,2,130031 -2459,2,130041 -2460,2,130051 -2461,3,130022 -2462,3,130032 -2463,3,130042 -2464,3,130052 -2465,4,130023 -2466,4,130033 -2467,4,130043 -2468,4,130053 -2469,5,130024 -2470,5,130034 -2471,5,130044 -2472,5,130054 -2473,3,130060 -2474,3,130070 -2475,3,130080 -2476,3,130090 -2477,3,130100 -2478,3,130110 -2479,3,130120 -2480,3,130130 -2481,3,130140 -2482,3,130150 -2483,3,130160 -2484,3,130170 -2485,3,130180 -2486,3,130190 -2487,3,130200 -2488,3,130420 -2489,3,130510 -2490,3,130520 -2491,3,130530 -2492,3,130540 -2493,4,140000 -2494,4,150010 -2495,4,150020 -2496,4,150030 -2497,4,150040 -2500,1,101000000 -2501,2,101000001 -2502,3,101000002 -2503,4,101000008 -2504,5,101000004 -2505,1,102000000 -2506,2,102000001 -2507,3,102000002 -2508,4,102000003 -2509,5,102000004 -2510,1,103000000 -2511,2,103000001 -2512,3,103000002 -2513,4,103000003 -2514,5,103000004 -2515,1,105000000 -2516,2,105000001 -2517,3,105000002 -2518,4,105000003 -2519,5,105000004 -2520,1,108000000 -2521,2,108000001 -2522,3,108000002 -2523,4,108000003 -2524,5,108000004 -2525,1,109000000 -2526,2,109000001 -2527,3,109000002 -2528,4,109000003 -2529,5,109000004 -2530,1,111000000 -2531,2,111000001 -2532,3,111000002 -2533,4,111000003 -2534,5,111000004 -2535,1,112000000 -2536,2,112000001 -2537,3,112000002 -2538,4,112000003 -2539,5,112000004 -2540,1,120000000 -2541,2,120000001 -2542,3,120000002 -2543,4,120000003 -2544,5,120000004 -2545,1,101000010 -2546,2,101000020 -2547,2,101000030 -2548,3,101000040 -2549,3,101000050 -2550,3,101000060 -2551,3,101000070 -2552,3,101000080 -2553,1,102000010 -2554,2,102000020 -2555,2,102000030 -2556,2,102000040 -2557,3,102000050 -2558,3,102000060 -2559,3,102000070 -2560,3,102000080 -2561,1,103000010 -2562,2,103000020 -2563,2,103000030 -2564,2,103000040 -2565,3,103000050 -2566,3,103000060 -2567,3,103000070 -2568,3,103000080 -2569,1,104000010 -2570,2,104000020 -2571,2,104000030 -2572,2,104000040 -2573,3,104000050 -2574,3,104000060 -2575,3,104000070 -2576,3,104000080 -2577,1,105000010 -2578,2,105000020 -2579,2,105000030 -2580,2,105000040 -2581,3,105000050 -2582,3,105000060 -2583,3,105000070 -2584,3,105000080 -2585,1,106000010 -2586,2,106000020 -2587,2,106000030 -2588,2,106000040 -2589,3,106000050 -2590,3,106000060 -2591,3,106000070 -2592,3,106000080 -2593,1,107000010 -2594,2,107000020 -2595,2,107000030 -2596,2,107000040 -2597,3,107000050 -2598,3,107000060 -2599,3,107000070 -2600,3,107000080 -2601,1,108000010 -2602,2,108000020 -2603,2,108000030 -2604,2,108000040 -2605,3,108000050 -2606,3,108000060 -2607,3,108000070 -2608,3,108000080 -2609,2,180001 -2611,1,101000000 -2612,2,101000001 -2613,3,101000002 -2614,4,101000008 -2615,5,101000004 -2616,1,102000000 -2617,2,102000001 -2618,3,102000002 -2619,4,102000003 -2620,5,102000004 -2621,1,103000000 -2622,2,103000001 -2623,3,103000002 -2624,4,103000003 -2625,5,103000004 -2626,1,105000000 -2627,2,105000001 -2628,3,105000002 -2629,4,105000003 -2630,5,105000004 -2631,1,108000000 -2632,2,108000001 -2633,3,108000002 -2634,4,108000003 -2635,5,108000004 -2636,1,109000000 -2637,2,109000001 -2638,3,109000002 -2639,4,109000003 -2640,5,109000004 -2641,1,111000000 -2642,2,111000001 -2643,3,111000002 -2644,4,111000003 -2645,5,111000004 -2646,1,112000000 -2647,2,112000001 -2648,3,112000002 -2649,4,112000003 -2650,5,112000004 -2651,1,120000000 -2652,2,120000001 -2653,3,120000002 -2654,4,120000003 -2655,5,120000004 -2656,1,101000010 -2657,2,101000020 -2658,2,101000030 -2659,3,101000040 -2660,3,101000050 -2661,3,101000060 -2662,3,101000070 -2663,3,101000080 -2664,1,102000010 -2665,2,102000020 -2666,2,102000030 -2667,2,102000040 -2668,3,102000050 -2669,3,102000060 -2670,3,102000070 -2671,3,102000080 -2672,1,103000010 -2673,2,103000020 -2674,2,103000030 -2675,2,103000040 -2676,3,103000050 -2677,3,103000060 -2678,3,103000070 -2679,3,103000080 -2680,1,104000010 -2681,2,104000020 -2682,2,104000030 -2683,2,104000040 -2684,3,104000050 -2685,3,104000060 -2686,3,104000070 -2687,3,104000080 -2688,1,105000010 -2689,2,105000020 -2690,2,105000030 -2691,2,105000040 -2692,3,105000050 -2693,3,105000060 -2694,3,105000070 -2695,3,105000080 -2696,1,106000010 -2697,2,106000020 -2698,2,106000030 -2699,2,106000040 -2700,3,106000050 -2701,3,106000060 -2702,3,106000070 -2703,3,106000080 -2704,1,107000010 -2705,2,107000020 -2706,2,107000030 -2707,2,107000040 -2708,3,107000050 -2709,3,107000060 -2710,3,107000070 -2711,3,107000080 -2712,1,108000010 -2713,2,108000020 -2714,2,108000030 -2715,2,108000040 -2716,3,108000050 -2717,3,108000060 -2718,3,108000070 -2719,3,108000080 -2720,1,109000010 -2721,2,109000020 -2722,2,109000030 -2723,2,109000040 -2724,3,109000050 -2725,3,109000060 -2726,3,109000070 -2727,3,109000080 -2728,2,180001 -2731,1,101000000 -2732,2,101000001 -2733,3,101000002 -2734,4,101000008 -2735,5,101000004 -2736,1,102000000 -2737,2,102000001 -2738,3,102000002 -2739,4,102000003 -2740,5,102000004 -2741,1,103000000 -2742,2,103000001 -2743,3,103000002 -2744,4,103000003 -2745,5,103000004 -2746,1,105000000 -2747,2,105000001 -2748,3,105000002 -2749,4,105000003 -2750,5,105000004 -2751,1,107000000 -2752,2,107000001 -2753,3,107000002 -2754,4,107000003 -2755,5,107000004 -2756,1,108000000 -2757,2,108000001 -2758,3,108000002 -2759,4,108000003 -2760,5,108000004 -2761,1,109000000 -2762,2,109000001 -2763,3,109000002 -2764,4,109000003 -2765,5,109000004 -2766,1,111000000 -2767,2,111000001 -2768,3,111000002 -2769,4,111000003 -2770,5,111000004 -2771,1,112000000 -2772,2,112000001 -2773,3,112000002 -2774,4,112000003 -2775,5,112000004 -2776,1,120000000 -2777,2,120000001 -2778,3,120000002 -2779,4,120000003 -2780,5,120000004 -2781,1,101000010 -2782,2,101000020 -2783,2,101000030 -2784,3,101000040 -2785,3,101000050 -2786,3,101000060 -2787,3,101000070 -2788,3,101000080 -2789,1,102000010 -2790,2,102000020 -2791,2,102000030 -2792,2,102000040 -2793,3,102000050 -2794,3,102000060 -2795,3,102000070 -2796,3,102000080 -2797,1,103000010 -2798,2,103000020 -2799,2,103000030 -2800,2,103000040 -2801,3,103000050 -2802,3,103000060 -2803,3,103000070 -2804,3,103000080 -2805,1,104000010 -2806,2,104000020 -2807,2,104000030 -2808,2,104000040 -2809,3,104000050 -2810,3,104000060 -2811,3,104000070 -2812,3,104000080 -2813,1,105000010 -2814,2,105000020 -2815,2,105000030 -2816,2,105000040 -2817,3,105000050 -2818,3,105000060 -2819,3,105000070 -2820,3,105000080 -2821,1,106000010 -2822,2,106000020 -2823,2,106000030 -2824,2,106000040 -2825,3,106000050 -2826,3,106000060 -2827,3,106000070 -2828,3,106000080 -2829,1,107000010 -2830,2,107000020 -2831,2,107000030 -2832,2,107000040 -2833,3,107000050 -2834,3,107000060 -2835,3,107000070 -2836,3,107000080 -2837,1,108000010 -2838,2,108000020 -2839,2,108000030 -2840,2,108000040 -2841,3,108000050 -2842,3,108000060 -2843,3,108000070 -2844,3,108000080 -2845,1,109000010 -2846,2,109000020 -2847,2,109000030 -2848,2,109000040 -2849,3,109000050 -2850,3,109000060 -2851,3,109000070 -2852,3,109000080 -2853,1,110000010 -2854,2,110000020 -2855,2,110000030 -2856,2,110000040 -2857,3,110000050 -2858,3,110000060 -2859,3,110000070 -2860,3,110000080 -2861,2,180001 -2863,1,107000000 -2864,2,107000001 -2865,3,107000002 -2866,4,107000003 -2867,5,107000004 -2868,1,120000000 -2869,2,120000001 -2870,3,120000002 -2871,4,120000003 -2872,5,120000004 -2874,3,110000070 -2875,3,110000080 -2876,4,110000100 -2877,5,110000110 -2878,1,107000000 -2879,2,107000001 -2880,3,107000002 -2881,4,107000003 -2882,5,107000004 -2883,1,120000000 -2884,2,120000001 -2885,3,120000002 -2886,4,120000003 -2887,5,120000004 -2888,3,120023 -2889,3,120033 -2890,3,120043 -2891,3,120053 -2892,4,120024 -2893,4,120034 -2894,4,120044 -2895,4,120054 -2896,3,120240 -2897,3,120250 -2898,3,120260 -2899,3,120270 -2900,3,120300 -2901,3,120310 -2902,3,120320 -2903,3,120330 -2904,3,120340 -2905,3,120350 -2906,3,120360 -2907,3,120370 -2908,3,120380 -2909,3,120390 -2910,3,120400 -2911,3,120410 -2912,3,120420 -2913,3,120430 -2914,3,120450 -2915,3,120460 -2916,3,120550 -2917,3,120560 -2918,3,120570 -2919,3,120990 -2920,3,121000 -2921,3,121010 -2922,3,121020 -2923,4,140000 -2924,4,150010 -2925,4,150020 -2926,4,150030 -2927,4,150040 -2929,3,108000060 -2930,3,108000070 -2931,3,108000080 -2932,3,107000050 -2933,4,108000100 -2934,4,105000090 -2935,5,108000110 -2936,1,108000000 -2937,2,108000001 -2938,3,108000002 -2939,4,108000003 -2940,5,108000004 -2941,1,120000000 -2942,2,120000001 -2943,3,120000002 -2944,4,120000003 -2945,5,120000004 -2946,3,170004 -2948,3,102000060 -2949,3,102000070 -2950,3,102000080 -2951,3,103000050 -2952,3,105000050 -2953,4,102000100 -2954,4,102000110 -2955,4,106000090 -2956,4,109000090 -2957,5,102000120 -2958,1,102000000 -2959,2,102000001 -2960,3,102000002 -2961,4,102000003 -2962,5,102000004 -2963,1,112000000 -2964,2,112000001 -2965,3,112000002 -2966,4,112000003 -2967,5,112000004 -2968,3,170004 -2970,3,101000050 -2971,3,101000060 -2972,3,101000080 -2973,3,103000060 -2974,3,103000070 -2975,3,103000080 -2976,3,101000040 -2977,3,109000060 -2978,3,109000070 -2979,3,109000080 -2980,3,110000050 -2981,4,101000090 -2982,4,102000090 -2983,4,103000100 -2984,4,101000100 -2985,4,101000110 -2986,4,109000100 -2987,5,101000120 -2988,5,103000120 -2989,5,109000110 -2990,1,101000000 -2991,2,101000001 -2992,3,101000002 -2993,4,101000008 -2994,5,101000004 -2995,1,112000000 -2996,2,112000001 -2997,3,112000002 -2998,4,112000003 -2999,5,112000004 -3000,3,120023 -3001,3,120033 -3002,3,120043 -3003,3,120053 -3004,4,120024 -3005,4,120034 -3006,4,120044 -3007,4,120054 -3008,3,120240 -3009,3,120250 -3010,3,120260 -3011,3,120270 -3012,3,120300 -3013,3,120310 -3014,3,120320 -3015,3,120330 -3016,3,120340 -3017,3,120350 -3018,3,120360 -3019,3,120370 -3020,3,120380 -3021,3,120390 -3022,3,120400 -3023,3,120410 -3024,3,120420 -3025,3,120430 -3026,3,120450 -3027,3,120460 -3028,3,120550 -3029,3,120560 -3030,3,120570 -3031,3,120990 -3032,3,121000 -3033,3,121010 -3034,3,121020 -3035,4,140000 -3036,4,150010 -3037,4,150020 -3038,4,150030 -3039,4,150040 -3041,3,105000060 -3042,3,105000070 -3043,3,105000080 -3044,3,104000050 -3045,3,106000050 -3046,4,105000100 -3047,4,105000110 -3048,4,108000090 -3049,4,110000090 -3050,5,105000120 -3051,1,109000000 -3052,2,109000001 -3053,3,109000002 -3054,4,109000003 -3055,5,109000004 -3056,1,112000000 -3057,2,112000001 -3058,3,112000002 -3059,4,112000003 -3060,5,112000004 -3061,3,170004 -3063,3,107000060 -3064,3,107000070 -3065,3,107000080 -3066,3,108000050 -3067,3,109000050 -3068,4,107000100 -3069,4,103000090 -3070,5,107000110 -3071,1,105000000 -3072,2,105000001 -3073,3,105000002 -3074,4,105000003 -3075,5,105000004 -3076,1,120000000 -3077,2,120000001 -3078,3,120000002 -3079,4,120000003 -3080,5,120000004 -3081,3,130023 -3082,3,130033 -3083,3,130043 -3084,3,130053 -3085,4,130024 -3086,4,130034 -3087,4,130044 -3088,4,130054 -3089,3,130060 -3090,3,130070 -3091,3,130080 -3092,3,130090 -3093,3,130100 -3094,3,130110 -3095,3,130120 -3096,3,130130 -3097,3,130140 -3098,3,130150 -3099,3,130160 -3100,3,130170 -3101,3,130180 -3102,3,130190 -3103,3,130200 -3104,3,130420 -3105,3,130510 -3106,3,130520 -3107,3,130530 -3108,3,130540 -3109,3,130660 -3110,4,140000 -3111,4,150010 -3112,4,150020 -3113,4,150030 -3114,4,150040 -3116,3,106000060 -3117,3,106000070 -3118,3,106000080 -3119,3,101000070 -3120,3,110000060 -3121,4,106000100 -3122,4,106000110 -3123,4,104000090 -3124,5,106000120 -3125,1,103000000 -3126,2,103000001 -3127,3,103000002 -3128,4,103000003 -3129,5,103000004 -3130,1,112000000 -3131,2,112000001 -3132,3,112000002 -3133,4,112000003 -3134,5,112000004 -3135,3,170004 -3137,3,104000060 -3138,3,104000070 -3139,3,104000080 -3140,3,102000050 -3141,4,104000100 -3142,4,104000110 -3143,4,107000090 -3144,5,104000120 -3145,1,111000000 -3146,2,111000001 -3147,3,111000002 -3148,4,111000003 -3149,5,111000004 -3150,1,120000000 -3151,2,120000001 -3152,3,120000002 -3153,4,120000003 -3154,5,120000004 -3155,3,110023 -3156,3,110033 -3157,3,110043 -3158,3,110053 -3159,4,110024 -3160,4,110034 -3161,4,110044 -3162,4,110054 -3163,3,110060 -3164,3,110070 -3165,3,110080 -3166,3,110090 -3167,3,110100 -3168,3,110110 -3169,3,110120 -3170,3,110130 -3171,3,110140 -3172,3,110150 -3173,3,110160 -3174,3,110170 -3175,3,110180 -3176,3,110190 -3177,3,110200 -3178,3,110210 -3179,3,110220 -3180,3,110230 -3181,3,110240 -3182,3,110250 -3183,3,110260 -3184,3,110270 -3185,3,110620 -3186,3,110670 -3187,4,140000 -3188,4,150010 -3189,4,150020 -3190,4,150030 -3191,4,150040 -3193,3,101000050 -3194,3,101000060 -3195,3,101000080 -3196,3,103000060 -3197,3,103000070 -3198,3,103000080 -3199,3,101000040 -3200,3,109000060 -3201,3,109000070 -3202,3,109000080 -3203,3,110000050 -3204,4,101000090 -3205,4,102000090 -3206,4,103000100 -3207,4,101000100 -3208,4,101000110 -3209,4,109000100 -3210,5,101000120 -3211,5,103000120 -3212,5,109000110 -3213,1,101000000 -3214,2,101000001 -3215,3,101000002 -3216,4,101000008 -3217,5,101000004 -3218,1,120000000 -3219,2,120000001 -3220,3,120000002 -3221,4,120000003 -3222,5,120000004 -3223,3,170004 -3225,3,110000070 -3226,3,110000080 -3227,4,110000100 -3228,5,110000110 -3229,1,107000000 -3230,2,107000001 -3231,3,107000002 -3232,4,107000003 -3233,5,107000004 -3234,1,120000000 -3235,2,120000001 -3236,3,120000002 -3237,4,120000003 -3238,5,120000004 -3239,3,180004 -3241,3,105000060 -3242,3,105000070 -3243,3,105000080 -3244,3,104000050 -3245,3,106000050 -3246,4,105000100 -3247,4,105000110 -3248,4,108000090 -3249,4,110000090 -3250,5,105000120 -3251,1,109000000 -3252,2,109000001 -3253,3,109000002 -3254,4,109000003 -3255,5,109000004 -3256,1,112000000 -3257,2,112000001 -3258,3,112000002 -3259,4,112000003 -3260,5,112000004 -3261,3,170004 -3263,3,108000060 -3264,3,108000070 -3265,3,108000080 -3266,3,107000050 -3267,4,108000100 -3268,4,105000090 -3269,5,108000110 -3270,1,108000000 -3271,2,108000001 -3272,3,108000002 -3273,4,108000003 -3274,5,108000004 -3275,1,120000000 -3276,2,120000001 -3277,3,120000002 -3278,4,120000003 -3279,5,120000004 -3280,4,120024 -3281,4,120034 -3282,4,120044 -3283,4,120054 -3284,3,120240 -3285,3,120250 -3286,3,120260 -3287,3,120270 -3288,3,120300 -3289,3,120310 -3290,3,120320 -3291,3,120330 -3292,3,120340 -3293,3,120350 -3294,3,120360 -3295,3,120370 -3296,3,120380 -3297,3,120390 -3298,3,120400 -3299,3,120410 -3300,3,120420 -3301,3,120430 -3302,3,120450 -3303,3,120460 -3304,3,120550 -3305,3,120560 -3306,3,120570 -3307,3,120990 -3308,3,121000 -3309,3,121010 -3310,3,121020 -3311,4,140000 -3312,4,150010 -3313,4,150020 -3314,4,150030 -3315,4,150040 -3317,3,104000060 -3318,3,104000070 -3319,3,104000080 -3320,3,102000050 -3321,4,104000100 -3322,4,104000110 -3323,4,107000090 -3324,5,104000120 -3325,1,111000000 -3326,2,111000001 -3327,3,111000002 -3328,4,111000003 -3329,5,111000004 -3330,1,120000000 -3331,2,120000001 -3332,3,120000002 -3333,4,120000003 -3334,5,120000004 -3335,4,110024 -3336,4,110034 -3337,4,110044 -3338,4,110054 -3339,3,110060 -3340,3,110070 -3341,3,110080 -3342,3,110090 -3343,3,110100 -3344,3,110110 -3345,3,110120 -3346,3,110130 -3347,3,110140 -3348,3,110150 -3349,3,110160 -3350,3,110170 -3351,3,110180 -3352,3,110190 -3353,3,110200 -3354,3,110210 -3355,3,110220 -3356,3,110230 -3357,3,110240 -3358,3,110250 -3359,3,110260 -3360,3,110270 -3361,3,110620 -3362,3,110670 -3363,4,140000 -3364,4,150010 -3365,4,150020 -3366,4,150030 -3367,4,150040 -3369,3,101000050 -3370,3,101000060 -3371,3,101000080 -3372,3,103000060 -3373,3,103000070 -3374,3,103000080 -3375,3,101000040 -3376,3,109000060 -3377,3,109000070 -3378,3,109000080 -3379,3,110000050 -3380,4,101000090 -3381,4,102000090 -3382,4,103000100 -3383,4,101000100 -3384,4,101000110 -3385,4,109000100 -3386,5,101000120 -3387,5,103000120 -3388,5,109000110 -3389,1,101000000 -3390,2,101000001 -3391,3,101000002 -3392,4,101000008 -3393,5,101000004 -3394,1,112000000 -3395,2,112000001 -3396,3,112000002 -3397,4,112000003 -3398,5,112000004 -3399,3,170004 -3401,3,107000060 -3402,3,107000070 -3403,3,107000080 -3404,3,108000050 -3405,3,109000050 -3406,4,107000100 -3407,4,103000090 -3408,5,107000110 -3409,1,105000000 -3410,2,105000001 -3411,3,105000002 -3412,4,105000003 -3413,5,105000004 -3414,1,120000000 -3415,2,120000001 -3416,3,120000002 -3417,4,120000003 -3418,5,120000004 -3419,4,120024 -3420,4,120034 -3421,4,120044 -3422,4,120054 -3423,3,120240 -3424,3,120250 -3425,3,120260 -3426,3,120270 -3427,3,120300 -3428,3,120310 -3429,3,120320 -3430,3,120330 -3431,3,120340 -3432,3,120350 -3433,3,120360 -3434,3,120370 -3435,3,120380 -3436,3,120390 -3437,3,120400 -3438,3,120410 -3439,3,120420 -3440,3,120430 -3441,3,120450 -3442,3,120460 -3443,3,120550 -3444,3,120560 -3445,3,120570 -3446,3,120990 -3447,3,121000 -3448,3,121010 -3449,3,121020 -3450,4,140000 -3451,4,150010 -3452,4,150020 -3453,4,150030 -3454,4,150040 -3456,3,108000060 -3457,3,108000070 -3458,3,108000080 -3459,3,107000050 -3460,4,108000100 -3461,4,105000090 -3462,5,108000110 -3463,1,108000000 -3464,2,108000001 -3465,3,108000002 -3466,4,108000003 -3467,5,108000004 -3468,1,120000000 -3469,2,120000001 -3470,3,120000002 -3471,4,120000003 -3472,5,120000004 -3473,3,170004 -3475,3,102000060 -3476,3,102000070 -3477,3,102000080 -3478,3,103000050 -3479,3,105000050 -3480,4,102000100 -3481,4,102000110 -3482,4,106000090 -3483,4,109000090 -3484,5,102000120 -3485,1,102000000 -3486,2,102000001 -3487,3,102000002 -3488,4,102000003 -3489,5,102000004 -3490,1,112000000 -3491,2,112000001 -3492,3,112000002 -3493,4,112000003 -3494,5,112000004 -3495,3,180004 -3497,3,106000060 -3498,3,106000070 -3499,3,106000080 -3500,3,101000070 -3501,3,110000060 -3502,4,106000100 -3503,4,106000110 -3504,4,104000090 -3505,5,106000120 -3506,1,103000000 -3507,2,103000001 -3508,3,103000002 -3509,4,103000003 -3510,5,103000004 -3511,1,112000000 -3512,2,112000001 -3513,3,112000002 -3514,4,112000003 -3515,5,112000004 -3516,4,130024 -3517,4,130034 -3518,4,130044 -3519,4,130054 -3520,3,130060 -3521,3,130070 -3522,3,130080 -3523,3,130090 -3524,3,130100 -3525,3,130110 -3526,3,130120 -3527,3,130130 -3528,3,130140 -3529,3,130150 -3530,3,130160 -3531,3,130170 -3532,3,130180 -3533,3,130190 -3534,3,130200 -3535,3,130420 -3536,3,130510 -3537,3,130520 -3538,3,130530 -3539,3,130540 -3540,3,130660 -3541,4,140000 -3542,4,150010 -3543,4,150020 -3544,4,150030 -3545,4,150040 -3547,3,110000070 -3548,3,110000080 -3549,4,110000100 -3550,5,110000110 -3551,1,107000000 -3552,2,107000001 -3553,3,107000002 -3554,4,107000003 -3555,5,107000004 -3556,1,120000000 -3557,2,120000001 -3558,3,120000002 -3559,4,120000003 -3560,5,120000004 -3561,3,170004 -3563,3,105000060 -3564,3,105000070 -3565,3,105000080 -3566,3,104000050 -3567,3,106000050 -3568,4,105000100 -3569,4,105000110 -3570,4,108000090 -3571,4,110000090 -3572,5,105000120 -3573,1,109000000 -3574,2,109000001 -3575,3,109000002 -3576,4,109000003 -3577,5,109000004 -3578,1,112000000 -3579,2,112000001 -3580,3,112000002 -3581,4,112000003 -3582,5,112000004 -3583,4,120024 -3584,4,120034 -3585,4,120044 -3586,4,120054 -3587,3,120240 -3588,3,120250 -3589,3,120260 -3590,3,120270 -3591,3,120300 -3592,3,120310 -3593,3,120320 -3594,3,120330 -3595,3,120340 -3596,3,120350 -3597,3,120360 -3598,3,120370 -3599,3,120380 -3600,3,120390 -3601,3,120400 -3602,3,120410 -3603,3,120420 -3604,3,120430 -3605,3,120450 -3606,3,120460 -3607,3,120550 -3608,3,120560 -3609,3,120570 -3610,3,120990 -3611,3,121000 -3612,3,121010 -3613,3,121020 -3614,4,140000 -3615,4,150010 -3616,4,150020 -3617,4,150030 -3618,4,150040 -3620,1,170000 -3621,2,170001 -3622,3,170002 -3623,4,170003 -3624,5,170004 -3625,1,180000 -3626,2,180001 -3627,3,180002 -3628,4,180003 -3629,5,180004 -3630,4,140000 -3631,1,201000010 -3632,1,292000010 -3633,1,299000040 -3634,1,299000070 -3635,1,299000110 -3636,1,299000120 -3637,1,299000140 -3638,2,202000010 -3639,2,290000010 -3640,2,299000010 -3641,2,299000150 -3642,2,299000190 -3643,2,299000200 -3644,2,299000210 -3645,3,298000050 -3646,3,298000060 -3647,3,299000060 -3648,3,299000170 -3649,5,150010 -3650,5,150020 -3651,5,150030 -3652,5,150040 -3654,3,105000060 -3655,3,105000070 -3656,3,105000080 -3657,3,104000050 -3658,3,106000050 -3659,4,105000100 -3660,4,105000110 -3661,4,108000090 -3662,4,110000090 -3663,5,105000120 -3664,1,109000000 -3665,2,109000001 -3666,3,109000002 -3667,4,109000003 -3668,5,109000004 -3669,1,112000000 -3670,2,112000001 -3671,3,112000002 -3672,4,112000003 -3673,5,112000004 -3674,3,170004 -3676,3,108000060 -3677,3,108000070 -3678,3,108000080 -3679,3,107000050 -3680,4,108000100 -3681,4,105000090 -3682,5,108000110 -3683,1,108000000 -3684,2,108000001 -3685,3,108000002 -3686,4,108000003 -3687,5,108000004 -3688,1,120000000 -3689,2,120000001 -3690,3,120000002 -3691,4,120000003 -3692,5,120000004 -3693,3,180004 -3695,3,106000060 -3696,3,106000070 -3697,3,106000080 -3698,3,101000070 -3699,3,110000060 -3700,4,106000100 -3701,4,106000110 -3702,4,104000090 -3703,5,106000120 -3704,1,103000000 -3705,2,103000001 -3706,3,103000002 -3707,4,103000003 -3708,5,103000004 -3709,1,112000000 -3710,2,112000001 -3711,3,112000002 -3712,4,112000003 -3713,5,112000004 -3714,3,170004 -3716,3,104000170 -3717,1,115000000 -3718,2,115000001 -3719,3,115000002 -3720,4,115000003 -3721,5,115000004 -3722,1,120000000 -3723,2,120000001 -3724,3,120000002 -3725,4,120000003 -3726,5,120000004 -3727,4,120024 -3728,4,120034 -3729,4,120044 -3730,4,120054 -3731,3,120241 -3732,3,120251 -3733,3,120261 -3734,3,120271 -3735,3,120300 -3736,3,120310 -3737,3,120320 -3738,3,120330 -3739,3,120340 -3740,3,120350 -3741,3,120360 -3742,3,120370 -3743,3,120380 -3744,3,120390 -3745,3,120400 -3746,3,120410 -3747,3,120420 -3748,3,120430 -3749,3,120450 -3750,3,120460 -3751,3,120550 -3752,3,120560 -3753,3,120570 -3754,3,120990 -3755,3,121000 -3756,3,121010 -3757,3,121020 -3758,4,140000 -3759,4,150010 -3760,4,150020 -3761,4,150030 -3762,4,150040 -3764,3,102000060 -3765,3,102000070 -3766,3,102000080 -3767,3,103000050 -3768,3,105000050 -3769,4,102000100 -3770,4,102000110 -3771,4,106000090 -3772,4,109000090 -3773,5,102000120 -3774,1,102000000 -3775,2,102000001 -3776,3,102000002 -3777,4,102000003 -3778,5,102000004 -3779,1,112000000 -3780,2,112000001 -3781,3,112000002 -3782,4,112000003 -3783,5,112000004 -3784,4,110024 -3785,4,110034 -3786,4,110044 -3787,4,110054 -3788,3,110060 -3789,3,110070 -3790,3,110080 -3791,3,110090 -3792,3,110100 -3793,3,110110 -3794,3,110120 -3795,3,110130 -3796,3,110140 -3797,3,110150 -3798,3,110160 -3799,3,110170 -3800,3,110180 -3801,3,110190 -3802,3,110200 -3803,3,110210 -3804,3,110220 -3805,3,110230 -3806,3,110240 -3807,3,110250 -3808,3,110260 -3809,3,110270 -3810,3,110620 -3811,3,110670 -3812,4,140000 -3813,4,150010 -3814,4,150020 -3815,4,150030 -3816,4,150040 -3818,3,104000060 -3819,3,104000070 -3820,3,104000080 -3821,3,102000050 -3822,4,104000100 -3823,4,104000110 -3824,4,107000090 -3825,5,104000120 -3826,1,111000000 -3827,2,111000001 -3828,3,111000002 -3829,4,111000003 -3830,5,111000004 -3831,1,120000000 -3832,2,120000001 -3833,3,120000002 -3834,4,120000003 -3835,5,120000004 -3836,4,110024 -3837,4,110034 -3838,4,110044 -3839,4,110054 -3840,3,110060 -3841,3,110070 -3842,3,110080 -3843,3,110090 -3844,3,110100 -3845,3,110110 -3846,3,110120 -3847,3,110130 -3848,3,110140 -3849,3,110150 -3850,3,110160 -3851,3,110170 -3852,3,110180 -3853,3,110190 -3854,3,110200 -3855,3,110210 -3856,3,110220 -3857,3,110230 -3858,3,110240 -3859,3,110250 -3860,3,110260 -3861,3,110270 -3862,3,110620 -3863,3,110670 -3864,4,140000 -3865,4,150010 -3866,4,150020 -3867,4,150030 -3868,4,150040 -3870,3,110000070 -3871,3,110000080 -3872,4,110000100 -3873,5,110000110 -3874,1,107000000 -3875,2,107000001 -3876,3,107000002 -3877,4,107000003 -3878,5,107000004 -3879,1,120000000 -3880,2,120000001 -3881,3,120000002 -3882,4,120000003 -3883,5,120000004 -3884,4,130024 -3885,4,130034 -3886,4,130044 -3887,4,130054 -3888,3,130060 -3889,3,130070 -3890,3,130080 -3891,3,130090 -3892,3,130100 -3893,3,130110 -3894,3,130120 -3895,3,130130 -3896,3,130140 -3897,3,130150 -3898,3,130160 -3899,3,130170 -3900,3,130180 -3901,3,130190 -3902,3,130200 -3903,3,130420 -3904,3,130510 -3905,3,130520 -3906,3,130530 -3907,3,130540 -3908,3,130660 -3909,4,140000 -3910,4,150010 -3911,4,150020 -3912,4,150030 -3913,4,150040 -3915,3,101000050 -3916,3,101000060 -3917,3,101000080 -3918,3,103000060 -3919,3,103000070 -3920,3,103000080 -3921,3,101000040 -3922,3,109000060 -3923,3,109000070 -3924,3,109000080 -3925,3,110000050 -3926,4,101000090 -3927,4,102000090 -3928,4,103000100 -3929,4,101000100 -3930,4,101000110 -3931,4,109000100 -3932,5,101000120 -3933,5,101000160 -3934,5,103000120 -3935,5,109000110 -3936,1,101000000 -3937,2,101000001 -3938,3,101000002 -3939,4,101000008 -3940,5,101000004 -3941,1,120000000 -3942,2,120000001 -3943,3,120000002 -3944,4,120000003 -3945,5,120000004 -3946,3,170004 -3948,3,107000060 -3949,3,107000070 -3950,3,107000080 -3951,3,108000050 -3952,3,109000050 -3953,4,107000100 -3954,4,103000090 -3955,5,107000110 -3956,1,105000000 -3957,2,105000001 -3958,3,105000002 -3959,4,105000003 -3960,5,105000004 -3961,1,120000000 -3962,2,120000001 -3963,3,120000002 -3964,4,120000003 -3965,5,120000004 -3966,3,170004 -3968,3,104000170 -3969,1,115000000 -3970,2,115000001 -3971,3,115000002 -3972,4,115000003 -3973,5,115000004 -3974,1,120000000 -3975,2,120000001 -3976,3,120000002 -3977,4,120000003 -3978,5,120000004 -3979,4,120024 -3980,4,120034 -3981,4,120044 -3982,4,120054 -3983,3,120241 -3984,3,120251 -3985,3,120261 -3986,3,120271 -3987,3,120300 -3988,3,120310 -3989,3,120320 -3990,3,120330 -3991,3,120340 -3992,3,120350 -3993,3,120360 -3994,3,120370 -3995,3,120380 -3996,3,120390 -3997,3,120400 -3998,3,120410 -3999,3,120420 -4000,3,120430 -4001,3,120450 -4002,3,120460 -4003,3,120550 -4004,3,120560 -4005,3,120570 -4006,3,120990 -4007,3,121000 -4008,3,121010 -4009,3,121020 -4010,4,140000 -4011,4,150010 -4012,4,150020 -4013,4,150030 -4014,4,150040 -4016,1,101000000 -4017,2,101000001 -4018,3,101000002 -4019,4,101000008 -4020,5,101000012 -4021,1,120000000 -4022,2,120000001 -4023,3,120000002 -4024,4,120000003 -4025,5,120000004 -4027,1,101000000 -4028,2,101000001 -4029,3,101000002 -4030,4,101000008 -4031,5,101000011 -4032,1,120000000 -4033,2,120000001 -4034,3,120000002 -4035,4,120000003 -4036,5,120000004 -4038,3,101000050 -4039,3,101000060 -4040,3,101000080 -4041,3,103000060 -4042,3,103000070 -4043,3,103000080 -4044,3,101000040 -4045,3,109000060 -4046,3,109000070 -4047,3,109000080 -4048,3,110000050 -4049,4,101000090 -4050,4,102000090 -4051,4,103000100 -4052,4,101000100 -4053,4,101000110 -4054,4,109000100 -4055,5,101000120 -4056,5,101000160 -4057,5,103000120 -4058,5,109000110 -4059,1,101000000 -4060,2,101000001 -4061,3,101000002 -4062,4,101000008 -4063,5,101000004 -4064,1,120000000 -4065,2,120000001 -4066,3,120000002 -4067,4,120000003 -4068,5,120000004 -4069,4,120024 -4070,4,120034 -4071,4,120044 -4072,4,120054 -4073,3,120241 -4074,3,120251 -4075,3,120261 -4076,3,120271 -4077,3,120300 -4078,3,120310 -4079,3,120320 -4080,3,120330 -4081,3,120340 -4082,3,120350 -4083,3,120360 -4084,3,120370 -4085,3,120380 -4086,3,120390 -4087,3,120400 -4088,3,120410 -4089,3,120420 -4090,3,120430 -4091,3,120450 -4092,3,120460 -4093,3,120550 -4094,3,120560 -4095,3,120570 -4096,3,120990 -4097,3,121000 -4098,3,121010 -4099,3,121020 -4100,4,140000 -4101,4,150010 -4102,4,150020 -4103,4,150030 -4104,4,150040 -4106,3,108000060 -4107,3,108000070 -4108,3,108000080 -4109,3,107000050 -4110,4,108000100 -4111,4,105000090 -4112,5,108000110 -4113,1,108000000 -4114,2,108000001 -4115,3,108000002 -4116,4,108000003 -4117,5,108000004 -4118,1,120000000 -4119,2,120000001 -4120,3,120000002 -4121,4,120000003 -4122,5,120000004 -4123,3,170004 -4125,3,102000060 -4126,3,102000070 -4127,3,102000080 -4128,3,103000050 -4129,3,105000050 -4130,4,102000100 -4131,4,102000110 -4132,4,106000090 -4133,4,109000090 -4134,5,102000120 -4135,1,102000000 -4136,2,102000001 -4137,3,102000002 -4138,4,102000003 -4139,5,102000004 -4140,1,112000000 -4141,2,112000001 -4142,3,112000002 -4143,4,112000003 -4144,5,112000004 -4145,4,110024 -4146,4,110034 -4147,4,110044 -4148,4,110054 -4149,3,110060 -4150,3,110070 -4151,3,110080 -4152,3,110090 -4153,3,110100 -4154,3,110110 -4155,3,110120 -4156,3,110130 -4157,3,110140 -4158,3,110150 -4159,3,110160 -4160,3,110170 -4161,3,110180 -4162,3,110190 -4163,3,110200 -4164,3,110210 -4165,3,110220 -4166,3,110230 -4167,3,110240 -4168,3,110250 -4169,3,110260 -4170,3,110270 -4171,3,110620 -4172,3,110670 -4173,4,140000 -4174,4,150010 -4175,4,150020 -4176,4,150030 -4177,4,150040 -4179,3,104000170 -4180,4,104000180 -4181,5,104000190 -4182,1,115000000 -4183,2,115000001 -4184,3,115000002 -4185,4,115000003 -4186,5,115000004 -4187,1,120000000 -4188,2,120000001 -4189,3,120000002 -4190,4,120000003 -4191,5,120000004 -4192,4,120024 -4193,4,120034 -4194,4,120044 -4195,4,120054 -4196,3,120241 -4197,3,120251 -4198,3,120261 -4199,3,120271 -4200,3,120300 -4201,3,120310 -4202,3,120320 -4203,3,120330 -4204,3,120340 -4205,3,120350 -4206,3,120360 -4207,3,120370 -4208,3,120380 -4209,3,120390 -4210,3,120400 -4211,3,120410 -4212,3,120420 -4213,3,120430 -4214,3,120450 -4215,3,120460 -4216,3,120550 -4217,3,120560 -4218,3,120570 -4219,3,120990 -4220,3,121000 -4221,3,121010 -4222,3,121020 -4223,4,140000 -4224,4,150010 -4225,4,150020 -4226,4,150030 -4227,4,150040 -4229,3,110000070 -4230,3,110000080 -4231,4,110000100 -4232,5,110000110 -4233,1,107000000 -4234,2,107000001 -4235,3,107000002 -4236,4,107000003 -4237,5,107000004 -4238,1,120000000 -4239,2,120000001 -4240,3,120000002 -4241,4,120000003 -4242,5,120000004 -4243,4,120024 -4244,4,120034 -4245,4,120044 -4246,4,120054 -4247,3,120241 -4248,3,120251 -4249,3,120261 -4250,3,120271 -4251,3,120300 -4252,3,120310 -4253,3,120320 -4254,3,120330 -4255,3,120340 -4256,3,120350 -4257,3,120360 -4258,3,120370 -4259,3,120380 -4260,3,120390 -4261,3,120400 -4262,3,120410 -4263,3,120420 -4264,3,120430 -4265,3,120450 -4266,3,120460 -4267,3,120550 -4268,3,120560 -4269,3,120570 -4270,3,120990 -4271,3,121000 -4272,3,121010 -4273,3,121020 -4274,4,140000 -4275,4,150010 -4276,4,150020 -4277,4,150030 -4278,4,150040 -4280,3,106000060 -4281,3,106000070 -4282,3,106000080 -4283,3,101000070 -4284,3,110000060 -4285,4,106000100 -4286,4,106000110 -4287,4,104000090 -4288,5,106000120 -4289,1,103000000 -4290,2,103000001 -4291,3,103000002 -4292,4,103000003 -4293,5,103000004 -4294,1,112000000 -4295,2,112000001 -4296,3,112000002 -4297,4,112000003 -4298,5,112000004 -4299,3,170004 -4301,3,105000060 -4302,3,105000070 -4303,3,105000080 -4304,3,104000050 -4305,3,106000050 -4306,4,105000100 -4307,4,105000110 -4308,4,108000090 -4309,4,110000090 -4310,5,105000120 -4311,1,109000000 -4312,2,109000001 -4313,3,109000002 -4314,4,109000003 -4315,5,109000004 -4316,1,112000000 -4317,2,112000001 -4318,3,112000002 -4319,4,112000003 -4320,5,112000004 -4321,4,130024 -4322,4,130034 -4323,4,130044 -4324,4,130054 -4325,3,130060 -4326,3,130070 -4327,3,130080 -4328,3,130090 -4329,3,130100 -4330,3,130110 -4331,3,130120 -4332,3,130130 -4333,3,130140 -4334,3,130150 -4335,3,130160 -4336,3,130170 -4337,3,130180 -4338,3,130190 -4339,3,130200 -4340,3,130420 -4341,3,130510 -4342,3,130520 -4343,3,130530 -4344,3,130540 -4345,3,130660 -4346,4,140000 -4347,4,150010 -4348,4,150020 -4349,4,150030 -4350,4,150040 -4352,3,101000050 -4353,3,101000060 -4354,3,101000080 -4355,3,103000060 -4356,3,103000070 -4357,3,103000080 -4358,3,101000040 -4359,3,109000060 -4360,3,109000070 -4361,3,109000080 -4362,3,110000050 -4363,4,101000090 -4364,4,102000090 -4365,4,103000100 -4366,4,101000100 -4367,4,101000110 -4368,4,109000100 -4369,5,101000120 -4370,5,103000120 -4371,5,109000110 -4372,1,101000000 -4373,2,101000001 -4374,3,101000002 -4375,4,101000008 -4376,5,101000004 -4377,1,112000000 -4378,2,112000001 -4379,3,112000002 -4380,4,112000003 -4381,5,112000004 -4382,3,180004 -4384,3,104000060 -4385,3,104000070 -4386,3,104000080 -4387,3,102000050 -4388,4,104000100 -4389,4,104000110 -4390,4,107000090 -4391,5,104000120 -4392,1,111000000 -4393,2,111000001 -4394,3,111000002 -4395,4,111000003 -4396,5,111000004 -4397,1,120000000 -4398,2,120000001 -4399,3,120000002 -4400,4,120000003 -4401,5,120000004 -4402,4,110024 -4403,4,110034 -4404,4,110044 -4405,4,110054 -4406,3,110060 -4407,3,110070 -4408,3,110080 -4409,3,110090 -4410,3,110100 -4411,3,110110 -4412,3,110120 -4413,3,110130 -4414,3,110140 -4415,3,110150 -4416,3,110160 -4417,3,110170 -4418,3,110180 -4419,3,110190 -4420,3,110200 -4421,3,110210 -4422,3,110220 -4423,3,110230 -4424,3,110240 -4425,3,110250 -4426,3,110260 -4427,3,110270 -4428,3,110620 -4429,3,110670 -4430,4,140000 -4431,4,150010 -4432,4,150020 -4433,4,150030 -4434,4,150040 -4436,3,107000060 -4437,3,107000070 -4438,3,107000080 -4439,3,108000050 -4440,3,109000050 -4441,4,107000100 -4442,4,103000090 -4443,5,107000110 -4444,1,105000000 -4445,2,105000001 -4446,3,105000002 -4447,4,105000003 -4448,5,105000004 -4449,1,120000000 -4450,2,120000001 -4451,3,120000002 -4452,4,120000003 -4453,5,120000004 -4454,4,130024 -4455,4,130034 -4456,4,130044 -4457,4,130054 -4458,3,130060 -4459,3,130070 -4460,3,130080 -4461,3,130090 -4462,3,130100 -4463,3,130110 -4464,3,130120 -4465,3,130130 -4466,3,130140 -4467,3,130150 -4468,3,130160 -4469,3,130170 -4470,3,130180 -4471,3,130190 -4472,3,130200 -4473,3,130420 -4474,3,130510 -4475,3,130520 -4476,3,130530 -4477,3,130540 -4478,3,130660 -4479,4,140000 -4480,4,150010 -4481,4,150020 -4482,4,150030 -4483,4,150040 -4485,1,109000010 -4486,2,109000020 -4487,2,109000030 -4488,2,109000040 -4489,3,109000050 -4490,3,109000060 -4491,3,109000070 -4492,3,109000080 -4493,4,109000090 -4494,4,109000100 -4495,5,109000110 -4496,1,170000 -4497,2,170001 -4498,3,170002 -4499,4,170003 -4500,5,170004 -4501,1,180000 -4502,2,180001 -4503,3,180002 -4504,4,180003 -4505,5,180004 -4506,1,201000010 -4507,1,292000010 -4508,1,299000040 -4509,1,299000070 -4510,1,299000110 -4511,1,299000120 -4512,1,299000140 -4513,2,202000010 -4514,2,290000010 -4515,2,299000010 -4516,2,299000150 -4517,2,299000190 -4518,2,299000200 -4519,2,299000210 -4520,3,298000050 -4521,3,298000060 -4522,3,299000060 -4523,3,299000170 -4524,4,140000 -4525,5,150010 -4526,5,150020 -4527,5,150030 -4528,5,150040 -4530,1,109000010 -4531,2,109000020 -4532,2,109000030 -4533,2,109000040 -4534,3,109000050 -4535,3,109000060 -4536,3,109000070 -4537,3,109000080 -4538,4,109000090 -4539,4,109000100 -4540,5,109000110 -4541,1,170000 -4542,2,170001 -4543,3,170002 -4544,4,170003 -4545,5,170004 -4546,1,180000 -4547,2,180001 -4548,3,180002 -4549,4,180003 -4550,5,180004 -4551,1,201000010 -4552,1,292000010 -4553,1,299000040 -4554,1,299000070 -4555,1,299000110 -4556,1,299000120 -4557,1,299000140 -4558,2,202000010 -4559,2,290000010 -4560,2,299000010 -4561,2,299000150 -4562,2,299000190 -4563,2,299000200 -4564,2,299000210 -4565,3,298000050 -4566,3,298000060 -4567,3,299000060 -4568,3,299000170 -4569,4,140000 -4570,5,150010 -4571,5,150020 -4572,5,150030 -4573,5,150040 -4575,3,109000050 -4576,3,109000060 -4577,3,109000070 -4578,3,109000080 -4579,4,109000090 -4580,4,109000100 -4581,5,109000110 -4582,1,170000 -4583,2,170001 -4584,3,170002 -4585,4,170003 -4586,5,170004 -4587,1,180000 -4588,2,180001 -4589,3,180002 -4590,4,180003 -4591,5,180004 -4592,1,201000010 -4593,1,292000010 -4594,1,299000040 -4595,1,299000070 -4596,1,299000110 -4597,1,299000120 -4598,1,299000140 -4599,2,202000010 -4600,2,290000010 -4601,2,299000010 -4602,2,299000150 -4603,2,299000190 -4604,2,299000200 -4605,2,299000210 -4606,3,298000050 -4607,3,298000060 -4608,3,299000060 -4609,3,299000170 -4610,4,140000 -4611,5,150010 -4612,5,150020 -4613,5,150030 -4614,5,150040 -4616,3,109000050 -4617,3,109000060 -4618,3,109000070 -4619,3,109000080 -4620,4,109000090 -4621,4,109000100 -4622,5,109000110 -4623,1,170000 -4624,2,170001 -4625,3,170002 -4626,4,170003 -4627,5,170004 -4628,1,180000 -4629,2,180001 -4630,3,180002 -4631,4,180003 -4632,5,180004 -4633,1,201000010 -4634,1,292000010 -4635,1,299000040 -4636,1,299000070 -4637,1,299000110 -4638,1,299000120 -4639,1,299000140 -4640,2,202000010 -4641,2,290000010 -4642,2,299000010 -4643,2,299000150 -4644,2,299000190 -4645,2,299000200 -4646,2,299000210 -4647,3,298000050 -4648,3,298000060 -4649,3,299000060 -4650,3,299000170 -4651,4,140000 -4652,5,150010 -4653,5,150020 -4654,5,150030 -4655,5,150040 -4657,3,109000050 -4658,3,109000060 -4659,3,109000070 -4660,3,109000080 -4661,4,109000090 -4662,4,109000100 -4663,5,109000110 -4664,1,170000 -4665,2,170001 -4666,3,170002 -4667,4,170003 -4668,5,170004 -4669,1,180000 -4670,2,180001 -4671,3,180002 -4672,4,180003 -4673,5,180004 -4674,1,201000010 -4675,1,292000010 -4676,1,299000040 -4677,1,299000070 -4678,1,299000110 -4679,1,299000120 -4680,1,299000140 -4681,2,202000010 -4682,2,290000010 -4683,2,299000010 -4684,2,299000150 -4685,2,299000190 -4686,2,299000200 -4687,2,299000210 -4688,3,298000050 -4689,3,298000060 -4690,3,299000060 -4691,3,299000170 -4692,4,140000 -4693,5,150010 -4694,5,150020 -4695,5,150030 -4696,5,150040 -4697,5,190000 -4698,5,200000 -4699,5,210000 -4701,3,102000060 -4702,3,102000070 -4703,3,102000080 -4704,3,103000050 -4705,3,105000050 -4706,4,102000100 -4707,4,102000110 -4708,4,106000090 -4709,4,109000090 -4710,5,102000120 -4711,1,102000000 -4712,2,102000001 -4713,3,102000002 -4714,4,102000003 -4715,5,102000004 -4716,1,112000000 -4717,2,112000001 -4718,3,112000002 -4719,4,112000003 -4720,5,112000004 -4721,3,170004 -4722,4,170005 -4724,3,110000070 -4725,3,110000080 -4726,4,110000100 -4727,5,110000110 -4728,1,107000000 -4729,2,107000001 -4730,3,107000002 -4731,4,107000003 -4732,5,107000004 -4733,1,120000000 -4734,2,120000001 -4735,3,120000002 -4736,4,120000003 -4737,5,120000004 -4738,4,120024 -4739,4,120034 -4740,4,120044 -4741,4,120054 -4742,3,120241 -4743,3,120251 -4744,3,120261 -4745,3,120271 -4746,3,120300 -4747,3,120310 -4748,3,120320 -4749,3,120330 -4750,3,120340 -4751,3,120350 -4752,3,120360 -4753,3,120370 -4754,3,120380 -4755,3,120390 -4756,3,120400 -4757,3,120410 -4758,3,120420 -4759,3,120430 -4760,3,120450 -4761,3,120460 -4762,3,120550 -4763,3,120560 -4764,3,120570 -4765,3,120990 -4766,3,121000 -4767,3,121010 -4768,3,121020 -4769,4,140000 -4770,4,150010 -4771,4,150020 -4772,4,150030 -4773,4,150040 -4775,3,105000060 -4776,3,105000070 -4777,3,105000080 -4778,3,104000050 -4779,3,106000050 -4780,4,105000100 -4781,4,105000110 -4782,4,108000090 -4783,4,110000090 -4784,5,105000120 -4785,1,109000000 -4786,2,109000001 -4787,3,109000002 -4788,4,109000003 -4789,5,109000004 -4790,1,112000000 -4791,2,112000001 -4792,3,112000002 -4793,4,112000003 -4794,5,112000004 -4795,4,130024 -4796,4,130034 -4797,4,130044 -4798,4,130054 -4799,3,130060 -4800,3,130070 -4801,3,130080 -4802,3,130090 -4803,3,130100 -4804,3,130110 -4805,3,130120 -4806,3,130130 -4807,3,130140 -4808,3,130150 -4809,3,130160 -4810,3,130170 -4811,3,130180 -4812,3,130190 -4813,3,130200 -4814,3,130420 -4815,3,130510 -4816,3,130520 -4817,3,130530 -4818,3,130540 -4819,3,130660 -4820,4,140000 -4821,4,150010 -4822,4,150020 -4823,4,150030 -4824,4,150040 -4826,3,104000060 -4827,3,104000070 -4828,3,104000080 -4829,3,102000050 -4830,4,104000100 -4831,4,104000110 -4832,4,107000090 -4833,5,104000120 -4834,1,111000000 -4835,2,111000001 -4836,3,111000002 -4837,4,111000003 -4838,5,111000004 -4839,1,120000000 -4840,2,120000001 -4841,3,120000002 -4842,4,120000003 -4843,5,120000004 -4844,4,110024 -4845,4,110034 -4846,4,110044 -4847,4,110054 -4848,3,110060 -4849,3,110070 -4850,3,110080 -4851,3,110090 -4852,3,110100 -4853,3,110110 -4854,3,110120 -4855,3,110130 -4856,3,110140 -4857,3,110150 -4858,3,110160 -4859,3,110170 -4860,3,110180 -4861,3,110190 -4862,3,110200 -4863,3,110210 -4864,3,110220 -4865,3,110230 -4866,3,110240 -4867,3,110250 -4868,3,110260 -4869,3,110270 -4870,3,110620 -4871,3,110670 -4872,4,140000 -4873,4,150010 -4874,4,150020 -4875,4,150030 -4876,4,150040 -4878,3,107000060 -4879,3,107000070 -4880,3,107000080 -4881,3,108000050 -4882,3,109000050 -4883,4,107000100 -4884,4,103000090 -4885,5,107000110 -4886,1,105000000 -4887,2,105000001 -4888,3,105000002 -4889,4,105000003 -4890,5,105000004 -4891,1,120000000 -4892,2,120000001 -4893,3,120000002 -4894,4,120000003 -4895,5,120000004 -4896,3,180004 -4897,4,180005 -4899,3,106000060 -4900,3,106000070 -4901,3,106000080 -4902,3,101000070 -4903,3,110000060 -4904,4,106000100 -4905,4,106000110 -4906,4,104000090 -4907,5,106000120 -4908,1,103000000 -4909,2,103000001 -4910,3,103000002 -4911,4,103000003 -4912,5,103000004 -4913,1,112000000 -4914,2,112000001 -4915,3,112000002 -4916,4,112000003 -4917,5,112000004 -4918,3,170004 -4919,4,170005 -4921,3,112000020 -4922,3,112000030 -4923,4,112000040 -4924,5,112000060 -4925,1,101000000 -4926,2,101000001 -4927,3,101000002 -4928,4,101000008 -4929,5,101000004 -4930,1,112000000 -4931,2,112000001 -4932,3,112000002 -4933,4,112000003 -4934,5,112000004 -4935,3,170004 -4936,4,170005 -4938,3,104000170 -4939,4,104000180 -4940,5,104000190 -4941,1,115000000 -4942,2,115000001 -4943,3,115000002 -4944,4,115000003 -4945,5,115000004 -4946,1,120000000 -4947,2,120000001 -4948,3,120000002 -4949,4,120000003 -4950,5,120000004 -4951,4,120024 -4952,4,120034 -4953,4,120044 -4954,4,120054 -4955,3,120241 -4956,3,120251 -4957,3,120261 -4958,3,120271 -4959,3,120300 -4960,3,120310 -4961,3,120320 -4962,3,120330 -4963,3,120340 -4964,3,120350 -4965,3,120360 -4966,3,120370 -4967,3,120380 -4968,3,120390 -4969,3,120400 -4970,3,120410 -4971,3,120420 -4972,3,120430 -4973,3,120450 -4974,3,120460 -4975,3,120550 -4976,3,120560 -4977,3,120570 -4978,3,120990 -4979,3,121000 -4980,3,121010 -4981,3,121020 -4982,4,140000 -4983,4,150010 -4984,4,150020 -4985,4,150030 -4986,4,150040 -4988,3,111000010 -4989,3,111000020 -4990,3,111000030 -4991,4,111000040 -4992,5,111000060 -4993,1,101000000 -4994,2,101000001 -4995,3,101000002 -4996,4,101000008 -4997,5,101000004 -4998,1,120000000 -4999,2,120000001 -5000,3,120000002 -5001,4,120000003 -5002,5,120000004 -5003,4,110024 -5004,4,110034 -5005,4,110044 -5006,4,110054 -5007,3,110060 -5008,3,110070 -5009,3,110080 -5010,3,110090 -5011,3,110100 -5012,3,110110 -5013,3,110120 -5014,3,110130 -5015,3,110140 -5016,3,110150 -5017,3,110160 -5018,3,110170 -5019,3,110180 -5020,3,110190 -5021,3,110200 -5022,3,110210 -5023,3,110220 -5024,3,110230 -5025,3,110240 -5026,3,110250 -5027,3,110260 -5028,3,110270 -5029,3,110620 -5030,3,110670 -5031,4,140000 -5032,4,150010 -5033,4,150020 -5034,4,150030 -5035,4,150040 -5037,3,108000060 -5038,3,108000070 -5039,3,108000080 -5040,3,107000050 -5041,3,112000010 -5042,4,108000100 -5043,4,105000090 -5044,5,108000110 -5045,1,108000000 -5046,2,108000001 -5047,3,108000002 -5048,4,108000003 -5049,5,108000004 -5050,1,120000000 -5051,2,120000001 -5052,3,120000002 -5053,4,120000003 -5054,5,120000004 -5055,4,130024 -5056,4,130034 -5057,4,130044 -5058,4,130054 -5059,3,130060 -5060,3,130070 -5061,3,130080 -5062,3,130090 -5063,3,130100 -5064,3,130110 -5065,3,130120 -5066,3,130130 -5067,3,130140 -5068,3,130150 -5069,3,130160 -5070,3,130170 -5071,3,130180 -5072,3,130190 -5073,3,130200 -5074,3,130420 -5075,3,130510 -5076,3,130520 -5077,3,130530 -5078,3,130540 -5079,3,130660 -5080,4,140000 -5081,4,150010 -5082,4,150020 -5083,4,150030 -5084,4,150040 -5086,3,111000010 -5087,3,111000020 -5088,3,111000030 -5089,4,111000040 -5090,5,111000060 -5091,1,170002 -5092,2,170003 -5093,3,170004 -5094,1,180002 -5095,2,180003 -5096,3,180004 -5097,1,201000010 -5098,1,292000010 -5099,1,299000040 -5100,1,299000070 -5101,1,299000110 -5102,1,299000120 -5103,1,299000140 -5104,2,202000010 -5105,2,290000010 -5106,2,299000010 -5107,2,299000150 -5108,2,299000190 -5109,2,299000200 -5110,2,299000210 -5111,3,298000050 -5112,3,298000060 -5113,3,299000060 -5114,3,299000170 -5115,4,140000 -5116,5,150010 -5117,5,150020 -5118,5,150030 -5119,5,150040 -5121,3,111000010 -5122,3,111000020 -5123,3,111000030 -5124,4,111000040 -5125,5,111000060 -5126,2,170003 -5127,3,170004 -5128,2,180003 -5129,3,180004 -5130,1,201000010 -5131,1,292000010 -5132,1,299000040 -5133,1,299000070 -5134,1,299000110 -5135,1,299000120 -5136,1,299000140 -5137,2,202000010 -5138,2,290000010 -5139,2,299000010 -5140,2,299000150 -5141,2,299000190 -5142,2,299000200 -5143,2,299000210 -5144,3,298000050 -5145,3,298000060 -5146,3,299000060 -5147,3,299000170 -5148,4,140000 -5149,5,150010 -5150,5,150020 -5151,5,150030 -5152,5,150040 -5154,3,111000010 -5155,3,111000020 -5156,3,111000030 -5157,4,111000040 -5158,5,111000060 -5159,3,170004 -5160,3,180004 -5161,1,201000010 -5162,1,292000010 -5163,1,299000040 -5164,1,299000070 -5165,1,299000110 -5166,1,299000120 -5167,1,299000140 -5168,2,202000010 -5169,2,290000010 -5170,2,299000010 -5171,2,299000150 -5172,2,299000190 -5173,2,299000200 -5174,2,299000210 -5175,3,298000050 -5176,3,298000060 -5177,3,299000060 -5178,3,299000170 -5179,4,140000 -5180,5,150010 -5181,5,150020 -5182,5,150030 -5183,5,150040 -5185,3,111000010 -5186,3,111000020 -5187,3,111000030 -5188,4,111000040 -5189,5,111000060 -5190,3,170004 -5191,3,180004 -5192,1,201000010 -5193,1,292000010 -5194,1,299000040 -5195,1,299000070 -5196,1,299000110 -5197,1,299000120 -5198,1,299000140 -5199,2,202000010 -5200,2,290000010 -5201,2,299000010 -5202,2,299000150 -5203,2,299000190 -5204,2,299000200 -5205,2,299000210 -5206,3,298000050 -5207,3,298000060 -5208,3,299000060 -5209,3,299000170 -5210,4,140000 -5211,5,150010 -5212,5,150020 -5213,5,150030 -5214,5,150040 -5216,3,111000010 -5217,3,111000020 -5218,3,111000030 -5219,4,111000040 -5220,5,111000060 -5221,3,170004 -5222,3,180004 -5223,1,201000010 -5224,1,292000010 -5225,1,299000040 -5226,1,299000070 -5227,1,299000110 -5228,1,299000120 -5229,1,299000140 -5230,2,202000010 -5231,2,290000010 -5232,2,299000010 -5233,2,299000150 -5234,2,299000190 -5235,2,299000200 -5236,2,299000210 -5237,3,298000050 -5238,3,298000060 -5239,3,299000060 -5240,3,299000170 -5241,4,140000 -5242,5,150010 -5243,5,150020 -5244,5,150030 -5245,5,150040 -5246,5,190000 -5247,5,200000 -5248,5,210000 -5250,3,101000050 -5251,3,101000060 -5252,3,101000080 -5253,3,101000040 -5254,3,109000060 -5255,3,109000070 -5256,3,109000080 -5257,3,105000060 -5258,3,105000070 -5259,3,105000080 -5260,3,104000050 -5261,3,106000050 -5262,4,101000090 -5263,4,101000100 -5264,4,101000110 -5265,4,109000100 -5266,4,105000100 -5267,4,105000110 -5268,4,108000090 -5269,4,110000090 -5270,5,101000120 -5271,5,109000110 -5272,5,105000120 -5273,1,101000000 -5274,2,101000001 -5275,3,101000002 -5276,4,101000008 -5277,5,101000004 -5278,1,109000000 -5279,2,109000001 -5280,3,109000002 -5281,4,109000003 -5282,5,109000004 -5283,3,170004 -5284,4,170005 -5285,3,180004 -5286,4,180005 -5287,4,140000 -5288,4,150010 -5289,4,150020 -5290,4,150030 -5291,4,150040 -5293,3,102000060 -5294,3,102000070 -5295,3,102000080 -5296,3,103000050 -5297,3,105000050 -5298,3,107000060 -5299,3,107000070 -5300,3,107000080 -5301,3,108000050 -5302,3,109000050 -5303,3,103000060 -5304,3,103000070 -5305,3,103000080 -5306,3,110000050 -5307,4,102000100 -5308,4,102000110 -5309,4,106000090 -5310,4,109000090 -5311,4,107000100 -5312,4,103000090 -5313,4,102000090 -5314,4,103000100 -5315,5,102000120 -5316,5,107000110 -5317,5,103000120 -5318,1,102000000 -5319,2,102000001 -5320,3,102000002 -5321,4,102000003 -5322,5,102000004 -5323,1,105000000 -5324,2,105000001 -5325,3,105000002 -5326,4,105000003 -5327,5,105000004 -5328,1,112000000 -5329,2,112000001 -5330,3,112000002 -5331,4,112000003 -5332,5,112000004 -5333,4,110024 -5334,4,110034 -5335,4,110044 -5336,4,110054 -5337,3,110060 -5338,3,110070 -5339,3,110080 -5340,3,110090 -5341,3,110100 -5342,3,110110 -5343,3,110120 -5344,3,110130 -5345,3,110140 -5346,3,110150 -5347,3,110160 -5348,3,110170 -5349,3,110180 -5350,3,110190 -5351,3,110200 -5352,3,110210 -5353,3,110220 -5354,3,110230 -5355,3,110240 -5356,3,110250 -5357,3,110260 -5358,3,110270 -5359,3,110620 -5360,3,110670 -5361,4,140000 -5362,4,150010 -5363,4,150020 -5364,4,150030 -5365,4,150040 -5367,3,106000060 -5368,3,106000070 -5369,3,106000080 -5370,3,101000070 -5371,3,110000060 -5372,3,104000060 -5373,3,104000070 -5374,3,104000080 -5375,3,102000050 -5376,3,104000170 -5377,3,104000260 -5378,4,106000100 -5379,4,106000110 -5380,4,104000090 -5381,4,104000100 -5382,4,104000110 -5383,4,107000090 -5384,4,104000180 -5385,4,104000270 -5386,5,106000120 -5387,5,104000120 -5388,5,104000190 -5389,1,103000000 -5390,2,103000001 -5391,3,103000002 -5392,4,103000003 -5393,5,103000004 -5394,1,111000000 -5395,2,111000001 -5396,3,111000002 -5397,4,111000003 -5398,5,111000004 -5399,1,115000000 -5400,2,115000001 -5401,3,115000002 -5402,4,115000003 -5403,5,115000004 -5404,4,120024 -5405,4,120034 -5406,4,120044 -5407,4,120054 -5408,3,120241 -5409,3,120251 -5410,3,120261 -5411,3,120271 -5412,3,120300 -5413,3,120310 -5414,3,120320 -5415,3,120330 -5416,3,120340 -5417,3,120350 -5418,3,120360 -5419,3,120370 -5420,3,120380 -5421,3,120390 -5422,3,120400 -5423,3,120410 -5424,3,120420 -5425,3,120430 -5426,3,120450 -5427,3,120460 -5428,3,120550 -5429,3,120560 -5430,3,120570 -5431,3,120990 -5432,3,121000 -5433,3,121010 -5434,3,121020 -5435,4,140000 -5436,4,150010 -5437,4,150020 -5438,4,150030 -5439,4,150040 -5441,3,111000010 -5442,3,111000020 -5443,3,111000030 -5444,3,112000020 -5445,3,112000030 -5446,3,108000060 -5447,3,108000070 -5448,3,108000080 -5449,3,107000050 -5450,3,112000010 -5451,3,110000070 -5452,3,110000080 -5453,4,111000040 -5454,4,112000040 -5455,4,108000100 -5456,4,105000090 -5457,4,110000100 -5458,5,111000060 -5459,5,112000060 -5460,5,108000110 -5461,5,110000110 -5462,1,108000000 -5463,2,108000001 -5464,3,108000002 -5465,4,108000003 -5466,5,108000004 -5467,1,107000000 -5468,2,107000001 -5469,3,107000002 -5470,4,107000003 -5471,5,107000004 -5472,1,120000000 -5473,2,120000001 -5474,3,120000002 -5475,4,120000003 -5476,5,120000004 -5477,4,130024 -5478,4,130034 -5479,4,130044 -5480,4,130054 -5481,3,130060 -5482,3,130070 -5483,3,130080 -5484,3,130090 -5485,3,130100 -5486,3,130110 -5487,3,130120 -5488,3,130130 -5489,3,130140 -5490,3,130150 -5491,3,130160 -5492,3,130170 -5493,3,130180 -5494,3,130190 -5495,3,130200 -5496,3,130420 -5497,3,130510 -5498,3,130520 -5499,3,130530 -5500,3,130540 -5501,3,130660 -5502,4,140000 -5503,4,150010 -5504,4,150020 -5505,4,150030 -5506,4,150040 -5508,1,101000000 -5509,2,101000001 -5510,3,101000002 -5511,4,101000008 -5512,5,101000004 -5513,1,120000000 -5514,2,120000001 -5515,3,120000002 -5516,4,120000003 -5517,5,120000004 -5519,1,101000010 -5520,1,102000010 -5521,1,103000010 -5522,1,104000010 -5523,1,105000010 -5524,1,106000010 -5525,1,107000010 -5526,1,108000010 -5527,1,109000010 -5528,1,110000010 -5529,2,101000020 -5530,2,101000030 -5531,2,102000020 -5532,2,102000030 -5533,2,102000040 -5534,2,103000020 -5535,2,103000030 -5536,2,103000040 -5537,2,104000020 -5538,2,104000030 -5539,2,104000040 -5540,2,105000020 -5541,2,105000030 -5542,2,105000040 -5543,2,106000020 -5544,2,106000030 -5545,2,106000040 -5546,2,107000020 -5547,2,107000030 -5548,2,107000040 -5549,2,108000020 -5550,2,108000030 -5551,2,108000040 -5552,2,109000020 -5553,2,109000030 -5554,2,109000040 -5555,2,110000020 -5556,2,110000030 -5557,2,110000040 -5558,3,101000050 -5559,3,101000060 -5560,3,101000080 -5561,3,101000040 -5562,3,109000060 -5563,3,109000070 -5564,3,109000080 -5565,3,105000060 -5566,3,105000070 -5567,3,105000080 -5568,3,104000050 -5569,3,106000050 -5570,3,102000060 -5571,3,102000070 -5572,3,102000080 -5573,3,103000050 -5574,3,105000050 -5575,3,107000060 -5576,3,107000070 -5577,3,107000080 -5578,3,108000050 -5579,3,109000050 -5580,3,103000060 -5581,3,103000070 -5582,3,103000080 -5583,3,110000050 -5584,3,106000060 -5585,3,106000070 -5586,3,106000080 -5587,3,101000070 -5588,3,110000060 -5589,3,104000060 -5590,3,104000070 -5591,3,104000080 -5592,3,102000050 -5593,3,104000170 -5594,3,104000260 -5595,3,111000010 -5596,3,111000020 -5597,3,111000030 -5598,3,112000020 -5599,3,112000030 -5600,3,108000060 -5601,3,108000070 -5602,3,108000080 -5603,3,107000050 -5604,3,112000010 -5605,3,110000070 -5606,3,110000080 -5607,4,101000090 -5608,4,101000100 -5609,4,101000110 -5610,4,109000100 -5611,4,105000100 -5612,4,105000110 -5613,4,108000090 -5614,4,110000090 -5615,4,102000100 -5616,4,102000110 -5617,4,106000090 -5618,4,109000090 -5619,4,107000100 -5620,4,103000090 -5621,4,102000090 -5622,4,103000100 -5623,4,106000100 -5624,4,106000110 -5625,4,104000090 -5626,4,104000100 -5627,4,104000110 -5628,4,107000090 -5629,4,104000180 -5630,4,111000040 -5631,4,112000040 -5632,4,108000100 -5633,4,105000090 -5634,4,110000100 -5635,5,101000120 -5636,5,109000110 -5637,5,105000120 -5638,5,102000120 -5639,5,107000110 -5640,5,103000120 -5641,5,106000120 -5642,5,104000120 -5643,5,104000190 -5644,5,111000060 -5645,5,112000060 -5646,5,108000110 -5647,5,110000110 -5648,1,170002 -5649,2,170003 -5650,3,170004 -5651,1,180002 -5652,2,180003 -5653,3,180004 -5654,1,201000010 -5655,1,292000010 -5656,1,299000040 -5657,1,299000070 -5658,1,299000110 -5659,1,299000120 -5660,1,299000140 -5661,2,202000010 -5662,2,290000010 -5663,2,299000010 -5664,2,299000150 -5665,2,299000190 -5666,2,299000200 -5667,2,299000210 -5668,3,298000050 -5669,3,298000060 -5670,3,299000060 -5671,3,299000170 -5672,5,297000100 -5673,5,291000020 -5674,4,140000 -5675,5,150010 -5676,5,150020 -5677,5,150030 -5678,5,150040 -5680,1,101000010 -5681,1,102000010 -5682,1,103000010 -5683,1,104000010 -5684,1,105000010 -5685,1,106000010 -5686,1,107000010 -5687,1,108000010 -5688,1,109000010 -5689,1,110000010 -5690,2,101000020 -5691,2,101000030 -5692,2,102000020 -5693,2,102000030 -5694,2,102000040 -5695,2,103000020 -5696,2,103000030 -5697,2,103000040 -5698,2,104000020 -5699,2,104000030 -5700,2,104000040 -5701,2,105000020 -5702,2,105000030 -5703,2,105000040 -5704,2,106000020 -5705,2,106000030 -5706,2,106000040 -5707,2,107000020 -5708,2,107000030 -5709,2,107000040 -5710,2,108000020 -5711,2,108000030 -5712,2,108000040 -5713,2,109000020 -5714,2,109000030 -5715,2,109000040 -5716,2,110000020 -5717,2,110000030 -5718,2,110000040 -5719,3,101000050 -5720,3,101000060 -5721,3,101000080 -5722,3,101000040 -5723,3,109000060 -5724,3,109000070 -5725,3,109000080 -5726,3,105000060 -5727,3,105000070 -5728,3,105000080 -5729,3,104000050 -5730,3,106000050 -5731,3,102000060 -5732,3,102000070 -5733,3,102000080 -5734,3,103000050 -5735,3,105000050 -5736,3,107000060 -5737,3,107000070 -5738,3,107000080 -5739,3,108000050 -5740,3,109000050 -5741,3,103000060 -5742,3,103000070 -5743,3,103000080 -5744,3,110000050 -5745,3,106000060 -5746,3,106000070 -5747,3,106000080 -5748,3,101000070 -5749,3,110000060 -5750,3,104000060 -5751,3,104000070 -5752,3,104000080 -5753,3,102000050 -5754,3,104000170 -5755,3,104000260 -5756,3,111000010 -5757,3,111000020 -5758,3,111000030 -5759,3,112000020 -5760,3,112000030 -5761,3,108000060 -5762,3,108000070 -5763,3,108000080 -5764,3,107000050 -5765,3,112000010 -5766,3,110000070 -5767,3,110000080 -5768,4,101000090 -5769,4,101000100 -5770,4,101000110 -5771,4,109000100 -5772,4,105000100 -5773,4,105000110 -5774,4,108000090 -5775,4,110000090 -5776,4,102000100 -5777,4,102000110 -5778,4,106000090 -5779,4,109000090 -5780,4,107000100 -5781,4,103000090 -5782,4,102000090 -5783,4,103000100 -5784,4,106000100 -5785,4,106000110 -5786,4,104000090 -5787,4,104000100 -5788,4,104000110 -5789,4,107000090 -5790,4,104000180 -5791,4,111000040 -5792,4,112000040 -5793,4,108000100 -5794,4,105000090 -5795,4,110000100 -5796,5,101000120 -5797,5,109000110 -5798,5,105000120 -5799,5,102000120 -5800,5,107000110 -5801,5,103000120 -5802,5,106000120 -5803,5,104000120 -5804,5,104000190 -5805,5,111000060 -5806,5,112000060 -5807,5,108000110 -5808,5,110000110 -5809,2,170003 -5810,3,170004 -5811,2,180003 -5812,3,180004 -5813,1,201000010 -5814,1,292000010 -5815,1,299000040 -5816,1,299000070 -5817,1,299000110 -5818,1,299000120 -5819,1,299000140 -5820,2,202000010 -5821,2,290000010 -5822,2,299000010 -5823,2,299000150 -5824,2,299000190 -5825,2,299000200 -5826,2,299000210 -5827,3,298000050 -5828,3,298000060 -5829,3,299000060 -5830,3,299000170 -5831,5,297000100 -5832,5,291000020 -5833,4,140000 -5834,5,150010 -5835,5,150020 -5836,5,150030 -5837,5,150040 -5839,3,101000050 -5840,3,101000060 -5841,3,101000080 -5842,3,101000040 -5843,3,109000060 -5844,3,109000070 -5845,3,109000080 -5846,3,105000060 -5847,3,105000070 -5848,3,105000080 -5849,3,104000050 -5850,3,106000050 -5851,3,102000060 -5852,3,102000070 -5853,3,102000080 -5854,3,103000050 -5855,3,105000050 -5856,3,107000060 -5857,3,107000070 -5858,3,107000080 -5859,3,108000050 -5860,3,109000050 -5861,3,103000060 -5862,3,103000070 -5863,3,103000080 -5864,3,110000050 -5865,3,106000060 -5866,3,106000070 -5867,3,106000080 -5868,3,101000070 -5869,3,110000060 -5870,3,104000060 -5871,3,104000070 -5872,3,104000080 -5873,3,102000050 -5874,3,104000170 -5875,3,104000260 -5876,3,111000010 -5877,3,111000020 -5878,3,111000030 -5879,3,112000020 -5880,3,112000030 -5881,3,108000060 -5882,3,108000070 -5883,3,108000080 -5884,3,107000050 -5885,3,112000010 -5886,3,110000070 -5887,3,110000080 -5888,4,101000090 -5889,4,101000100 -5890,4,101000110 -5891,4,109000100 -5892,4,105000100 -5893,4,105000110 -5894,4,108000090 -5895,4,110000090 -5896,4,102000100 -5897,4,102000110 -5898,4,106000090 -5899,4,109000090 -5900,4,107000100 -5901,4,103000090 -5902,4,102000090 -5903,4,103000100 -5904,4,106000100 -5905,4,106000110 -5906,4,104000090 -5907,4,104000100 -5908,4,104000110 -5909,4,107000090 -5910,4,104000180 -5911,4,111000040 -5912,4,112000040 -5913,4,108000100 -5914,4,105000090 -5915,4,110000100 -5916,5,101000120 -5917,5,109000110 -5918,5,105000120 -5919,5,102000120 -5920,5,107000110 -5921,5,103000120 -5922,5,106000120 -5923,5,104000120 -5924,5,104000190 -5925,5,111000060 -5926,5,112000060 -5927,5,108000110 -5928,5,110000110 -5929,3,170004 -5930,3,180004 -5931,1,201000010 -5932,1,292000010 -5933,1,299000040 -5934,1,299000070 -5935,1,299000110 -5936,1,299000120 -5937,1,299000140 -5938,2,202000010 -5939,2,290000010 -5940,2,299000010 -5941,2,299000150 -5942,2,299000190 -5943,2,299000200 -5944,2,299000210 -5945,3,298000050 -5946,3,298000060 -5947,3,299000060 -5948,3,299000170 -5949,5,297000100 -5950,5,291000020 -5951,4,140000 -5952,5,150010 -5953,5,150020 -5954,5,150030 -5955,5,150040 -5957,3,101000050 -5958,3,101000060 -5959,3,101000080 -5960,3,101000040 -5961,3,109000060 -5962,3,109000070 -5963,3,109000080 -5964,3,105000060 -5965,3,105000070 -5966,3,105000080 -5967,3,104000050 -5968,3,106000050 -5969,3,102000060 -5970,3,102000070 -5971,3,102000080 -5972,3,103000050 -5973,3,105000050 -5974,3,107000060 -5975,3,107000070 -5976,3,107000080 -5977,3,108000050 -5978,3,109000050 -5979,3,103000060 -5980,3,103000070 -5981,3,103000080 -5982,3,110000050 -5983,3,106000060 -5984,3,106000070 -5985,3,106000080 -5986,3,101000070 -5987,3,110000060 -5988,3,104000060 -5989,3,104000070 -5990,3,104000080 -5991,3,102000050 -5992,3,104000170 -5993,3,104000260 -5994,3,111000010 -5995,3,111000020 -5996,3,111000030 -5997,3,112000020 -5998,3,112000030 -5999,3,108000060 -6000,3,108000070 -6001,3,108000080 -6002,3,107000050 -6003,3,112000010 -6004,3,110000070 -6005,3,110000080 -6006,4,101000090 -6007,4,101000100 -6008,4,101000110 -6009,4,109000100 -6010,4,105000100 -6011,4,105000110 -6012,4,108000090 -6013,4,110000090 -6014,4,102000100 -6015,4,102000110 -6016,4,106000090 -6017,4,109000090 -6018,4,107000100 -6019,4,103000090 -6020,4,102000090 -6021,4,103000100 -6022,4,106000100 -6023,4,106000110 -6024,4,104000090 -6025,4,104000100 -6026,4,104000110 -6027,4,107000090 -6028,4,104000180 -6029,4,111000040 -6030,4,112000040 -6031,4,108000100 -6032,4,105000090 -6033,4,110000100 -6034,5,101000120 -6035,5,109000110 -6036,5,105000120 -6037,5,102000120 -6038,5,107000110 -6039,5,103000120 -6040,5,106000120 -6041,5,104000120 -6042,5,104000190 -6043,5,111000060 -6044,5,112000060 -6045,5,108000110 -6046,5,110000110 -6047,3,170004 -6048,3,180004 -6049,1,201000010 -6050,1,292000010 -6051,1,299000040 -6052,1,299000070 -6053,1,299000110 -6054,1,299000120 -6055,1,299000140 -6056,2,202000010 -6057,2,290000010 -6058,2,299000010 -6059,2,299000150 -6060,2,299000190 -6061,2,299000200 -6062,2,299000210 -6063,3,298000050 -6064,3,298000060 -6065,3,299000060 -6066,3,299000170 -6067,5,297000100 -6068,5,291000020 -6069,4,140000 -6070,5,150010 -6071,5,150020 -6072,5,150030 -6073,5,150040 -6075,3,101000050 -6076,3,101000060 -6077,3,101000080 -6078,3,101000040 -6079,3,109000060 -6080,3,109000070 -6081,3,109000080 -6082,3,105000060 -6083,3,105000070 -6084,3,105000080 -6085,3,104000050 -6086,3,106000050 -6087,3,102000060 -6088,3,102000070 -6089,3,102000080 -6090,3,103000050 -6091,3,105000050 -6092,3,107000060 -6093,3,107000070 -6094,3,107000080 -6095,3,108000050 -6096,3,109000050 -6097,3,103000060 -6098,3,103000070 -6099,3,103000080 -6100,3,110000050 -6101,3,106000060 -6102,3,106000070 -6103,3,106000080 -6104,3,101000070 -6105,3,110000060 -6106,3,104000060 -6107,3,104000070 -6108,3,104000080 -6109,3,102000050 -6110,3,104000170 -6111,3,104000260 -6112,3,111000010 -6113,3,111000020 -6114,3,111000030 -6115,3,112000020 -6116,3,112000030 -6117,3,108000060 -6118,3,108000070 -6119,3,108000080 -6120,3,107000050 -6121,3,112000010 -6122,3,110000070 -6123,3,110000080 -6124,4,101000090 -6125,4,101000100 -6126,4,101000110 -6127,4,109000100 -6128,4,105000100 -6129,4,105000110 -6130,4,108000090 -6131,4,110000090 -6132,4,102000100 -6133,4,102000110 -6134,4,106000090 -6135,4,109000090 -6136,4,107000100 -6137,4,103000090 -6138,4,102000090 -6139,4,103000100 -6140,4,106000100 -6141,4,106000110 -6142,4,104000090 -6143,4,104000100 -6144,4,104000110 -6145,4,107000090 -6146,4,104000180 -6147,4,111000040 -6148,4,112000040 -6149,4,108000100 -6150,4,105000090 -6151,4,110000100 -6152,5,101000120 -6153,5,109000110 -6154,5,105000120 -6155,5,102000120 -6156,5,107000110 -6157,5,103000120 -6158,5,106000120 -6159,5,104000120 -6160,5,104000190 -6161,5,111000060 -6162,5,112000060 -6163,5,108000110 -6164,5,110000110 -6165,3,170004 -6166,3,180004 -6167,1,201000010 -6168,1,292000010 -6169,1,299000040 -6170,1,299000070 -6171,1,299000110 -6172,1,299000120 -6173,1,299000140 -6174,2,202000010 -6175,2,290000010 -6176,2,299000010 -6177,2,299000150 -6178,2,299000190 -6179,2,299000200 -6180,2,299000210 -6181,3,298000050 -6182,3,298000060 -6183,3,299000060 -6184,3,299000170 -6185,5,297000100 -6186,5,291000020 -6187,4,140000 -6188,5,150010 -6189,5,150020 -6190,5,150030 -6191,5,150040 -6192,5,190000 -6193,5,200000 -6194,5,210000 -6196,3,111000010 -6197,3,111000020 -6198,3,111000030 -6199,3,112000020 -6200,3,112000030 -6201,3,108000060 -6202,3,108000070 -6203,3,108000080 -6204,3,107000050 -6205,3,112000010 -6206,3,110000070 -6207,3,110000080 -6208,4,111000040 -6209,4,112000040 -6210,4,108000100 -6211,4,105000090 -6212,4,110000100 -6213,5,111000060 -6214,5,112000060 -6215,5,108000110 -6216,5,110000110 -6217,1,108000000 -6218,2,108000001 -6219,3,108000002 -6220,4,108000003 -6221,5,108000004 -6222,1,107000000 -6223,2,107000001 -6224,3,107000002 -6225,4,107000003 -6226,5,107000004 -6227,1,120000000 -6228,2,120000001 -6229,3,120000002 -6230,4,120000003 -6231,5,120000004 -6232,3,170004 -6233,4,170005 -6234,3,180004 -6235,4,180005 -6236,4,140000 -6237,4,150010 -6238,4,150020 -6239,4,150030 -6240,4,150040 -6242,3,101000050 -6243,3,101000060 -6244,3,101000080 -6245,3,101000040 -6246,3,109000060 -6247,3,109000070 -6248,3,109000080 -6249,3,105000060 -6250,3,105000070 -6251,3,105000080 -6252,3,104000050 -6253,3,106000050 -6254,4,101000090 -6255,4,101000100 -6256,4,101000110 -6257,4,109000100 -6258,4,105000100 -6259,4,105000110 -6260,4,108000090 -6261,4,110000090 -6262,5,101000120 -6263,5,109000110 -6264,5,105000120 -6265,1,101000000 -6266,2,101000001 -6267,3,101000002 -6268,4,101000008 -6269,5,101000004 -6270,1,109000000 -6271,2,109000001 -6272,3,109000002 -6273,4,109000003 -6274,5,109000004 -6275,4,110024 -6276,4,110034 -6277,4,110044 -6278,4,110054 -6279,3,110060 -6280,3,110070 -6281,3,110080 -6282,3,110090 -6283,3,110100 -6284,3,110110 -6285,3,110120 -6286,3,110130 -6287,3,110140 -6288,3,110150 -6289,3,110160 -6290,3,110170 -6291,3,110180 -6292,3,110190 -6293,3,110200 -6294,3,110210 -6295,3,110220 -6296,3,110230 -6297,3,110240 -6298,3,110250 -6299,3,110260 -6300,3,110270 -6301,3,110620 -6302,3,110670 -6303,4,140000 -6304,4,150010 -6305,4,150020 -6306,4,150030 -6307,4,150040 -6309,3,102000060 -6310,3,102000070 -6311,3,102000080 -6312,3,103000050 -6313,3,105000050 -6314,3,107000060 -6315,3,107000070 -6316,3,107000080 -6317,3,108000050 -6318,3,109000050 -6319,3,103000060 -6320,3,103000070 -6321,3,103000080 -6322,3,110000050 -6323,4,102000100 -6324,4,102000110 -6325,4,106000090 -6326,4,109000090 -6327,4,107000100 -6328,4,103000090 -6329,4,102000090 -6330,4,103000100 -6331,5,102000120 -6332,5,107000110 -6333,5,103000120 -6334,1,102000000 -6335,2,102000001 -6336,3,102000002 -6337,4,102000003 -6338,5,102000004 -6339,1,105000000 -6340,2,105000001 -6341,3,105000002 -6342,4,105000003 -6343,5,105000004 -6344,1,112000000 -6345,2,112000001 -6346,3,112000002 -6347,4,112000003 -6348,5,112000004 -6349,4,120024 -6350,4,120034 -6351,4,120044 -6352,4,120054 -6353,3,120241 -6354,3,120251 -6355,3,120261 -6356,3,120271 -6357,3,120300 -6358,3,120310 -6359,3,120320 -6360,3,120330 -6361,3,120340 -6362,3,120350 -6363,3,120360 -6364,3,120370 -6365,3,120380 -6366,3,120390 -6367,3,120400 -6368,3,120410 -6369,3,120420 -6370,3,120430 -6371,3,120450 -6372,3,120460 -6373,3,120550 -6374,3,120560 -6375,3,120570 -6376,3,120990 -6377,3,121000 -6378,3,121010 -6379,3,121020 -6380,4,140000 -6381,4,150010 -6382,4,150020 -6383,4,150030 -6384,4,150040 -6386,3,106000060 -6387,3,106000070 -6388,3,106000080 -6389,3,101000070 -6390,3,110000060 -6391,3,104000060 -6392,3,104000070 -6393,3,104000080 -6394,3,102000050 -6395,3,104000170 -6396,3,104000260 -6397,4,106000100 -6398,4,106000110 -6399,4,104000090 -6400,4,104000100 -6401,4,104000110 -6402,4,107000090 -6403,4,104000180 -6404,5,106000120 -6405,5,104000120 -6406,5,104000190 -6407,1,103000000 -6408,2,103000001 -6409,3,103000002 -6410,4,103000003 -6411,5,103000004 -6412,1,111000000 -6413,2,111000001 -6414,3,111000002 -6415,4,111000003 -6416,5,111000004 -6417,1,115000000 -6418,2,115000001 -6419,3,115000002 -6420,4,115000003 -6421,5,115000004 -6422,4,130024 -6423,4,130034 -6424,4,130044 -6425,4,130054 -6426,3,130060 -6427,3,130070 -6428,3,130080 -6429,3,130090 -6430,3,130100 -6431,3,130110 -6432,3,130120 -6433,3,130130 -6434,3,130140 -6435,3,130150 -6436,3,130160 -6437,3,130170 -6438,3,130180 -6439,3,130190 -6440,3,130200 -6441,3,130420 -6442,3,130510 -6443,3,130520 -6444,3,130530 -6445,3,130540 -6446,3,130660 -6447,3,130790 -6448,3,130800 -6449,4,140000 -6450,4,150010 -6451,4,150020 -6452,4,150030 -6453,4,150040 -6455,1,101000010 -6456,1,102000010 -6457,1,103000010 -6458,1,104000010 -6459,1,105000010 -6460,1,106000010 -6461,1,107000010 -6462,1,108000010 -6463,1,109000010 -6464,1,110000010 -6465,2,101000020 -6466,2,101000030 -6467,2,102000020 -6468,2,102000030 -6469,2,102000040 -6470,2,103000020 -6471,2,103000030 -6472,2,103000040 -6473,2,104000020 -6474,2,104000030 -6475,2,104000040 -6476,2,105000020 -6477,2,105000030 -6478,2,105000040 -6479,2,106000020 -6480,2,106000030 -6481,2,106000040 -6482,2,107000020 -6483,2,107000030 -6484,2,107000040 -6485,2,108000020 -6486,2,108000030 -6487,2,108000040 -6488,2,109000020 -6489,2,109000030 -6490,2,109000040 -6491,2,110000020 -6492,2,110000030 -6493,2,110000040 -6494,2,118000010 -6495,3,101000050 -6496,3,101000060 -6497,3,101000080 -6498,3,101000040 -6499,3,109000060 -6500,3,109000070 -6501,3,109000080 -6502,3,105000060 -6503,3,105000070 -6504,3,105000080 -6505,3,104000050 -6506,3,106000050 -6507,3,102000060 -6508,3,102000070 -6509,3,102000080 -6510,3,103000050 -6511,3,105000050 -6512,3,107000060 -6513,3,107000070 -6514,3,107000080 -6515,3,108000050 -6516,3,109000050 -6517,3,103000060 -6518,3,103000070 -6519,3,103000080 -6520,3,110000050 -6521,3,106000060 -6522,3,106000070 -6523,3,106000080 -6524,3,101000070 -6525,3,110000060 -6526,3,104000060 -6527,3,104000070 -6528,3,104000080 -6529,3,102000050 -6530,3,104000170 -6531,3,104000260 -6532,3,111000010 -6533,3,111000020 -6534,3,111000030 -6535,3,112000020 -6536,3,112000030 -6537,3,108000060 -6538,3,108000070 -6539,3,108000080 -6540,3,107000050 -6541,3,112000010 -6542,3,110000070 -6543,3,110000080 -6544,3,118000020 -6545,3,118000030 -6546,3,118000040 -6547,4,101000090 -6548,4,101000100 -6549,4,101000110 -6550,4,109000100 -6551,4,105000100 -6552,4,105000110 -6553,4,108000090 -6554,4,110000090 -6555,4,102000100 -6556,4,102000110 -6557,4,106000090 -6558,4,109000090 -6559,4,107000100 -6560,4,103000090 -6561,4,102000090 -6562,4,103000100 -6563,4,106000100 -6564,4,106000110 -6565,4,104000090 -6566,4,104000100 -6567,4,104000110 -6568,4,107000090 -6569,4,104000180 -6570,4,111000040 -6571,4,112000040 -6572,4,108000100 -6573,4,105000090 -6574,4,110000100 -6575,4,118000050 -6576,4,118000060 -6577,5,101000120 -6578,5,109000110 -6579,5,105000120 -6580,5,102000120 -6581,5,107000110 -6582,5,103000120 -6583,5,106000120 -6584,5,104000120 -6585,5,104000190 -6586,5,111000060 -6587,5,112000060 -6588,5,108000110 -6589,5,110000110 -6590,5,118000070 -6591,1,170002 -6592,2,170003 -6593,3,170004 -6594,1,180002 -6595,2,180003 -6596,3,180004 -6597,1,201000010 -6598,1,292000010 -6599,1,299000040 -6600,1,299000070 -6601,1,299000110 -6602,1,299000120 -6603,1,299000140 -6604,2,202000010 -6605,2,290000010 -6606,2,299000010 -6607,2,299000150 -6608,2,299000190 -6609,2,299000200 -6610,2,299000210 -6611,3,298000050 -6612,3,298000060 -6613,3,299000060 -6614,3,299000170 -6615,5,297000100 -6616,5,291000020 -6617,4,140000 -6618,5,150010 -6619,5,150020 -6620,5,150030 -6621,5,150040 -6623,1,101000010 -6624,1,102000010 -6625,1,103000010 -6626,1,104000010 -6627,1,105000010 -6628,1,106000010 -6629,1,107000010 -6630,1,108000010 -6631,1,109000010 -6632,1,110000010 -6633,2,101000020 -6634,2,101000030 -6635,2,102000020 -6636,2,102000030 -6637,2,102000040 -6638,2,103000020 -6639,2,103000030 -6640,2,103000040 -6641,2,104000020 -6642,2,104000030 -6643,2,104000040 -6644,2,105000020 -6645,2,105000030 -6646,2,105000040 -6647,2,106000020 -6648,2,106000030 -6649,2,106000040 -6650,2,107000020 -6651,2,107000030 -6652,2,107000040 -6653,2,108000020 -6654,2,108000030 -6655,2,108000040 -6656,2,109000020 -6657,2,109000030 -6658,2,109000040 -6659,2,110000020 -6660,2,110000030 -6661,2,110000040 -6662,2,118000010 -6663,3,101000050 -6664,3,101000060 -6665,3,101000080 -6666,3,101000040 -6667,3,109000060 -6668,3,109000070 -6669,3,109000080 -6670,3,105000060 -6671,3,105000070 -6672,3,105000080 -6673,3,104000050 -6674,3,106000050 -6675,3,102000060 -6676,3,102000070 -6677,3,102000080 -6678,3,103000050 -6679,3,105000050 -6680,3,107000060 -6681,3,107000070 -6682,3,107000080 -6683,3,108000050 -6684,3,109000050 -6685,3,103000060 -6686,3,103000070 -6687,3,103000080 -6688,3,110000050 -6689,3,106000060 -6690,3,106000070 -6691,3,106000080 -6692,3,101000070 -6693,3,110000060 -6694,3,104000060 -6695,3,104000070 -6696,3,104000080 -6697,3,102000050 -6698,3,104000170 -6699,3,104000260 -6700,3,111000010 -6701,3,111000020 -6702,3,111000030 -6703,3,112000020 -6704,3,112000030 -6705,3,108000060 -6706,3,108000070 -6707,3,108000080 -6708,3,107000050 -6709,3,112000010 -6710,3,110000070 -6711,3,110000080 -6712,3,118000020 -6713,3,118000030 -6714,3,118000040 -6715,4,101000090 -6716,4,101000100 -6717,4,101000110 -6718,4,109000100 -6719,4,105000100 -6720,4,105000110 -6721,4,108000090 -6722,4,110000090 -6723,4,102000100 -6724,4,102000110 -6725,4,106000090 -6726,4,109000090 -6727,4,107000100 -6728,4,103000090 -6729,4,102000090 -6730,4,103000100 -6731,4,106000100 -6732,4,106000110 -6733,4,104000090 -6734,4,104000100 -6735,4,104000110 -6736,4,107000090 -6737,4,104000180 -6738,4,111000040 -6739,4,112000040 -6740,4,108000100 -6741,4,105000090 -6742,4,110000100 -6743,4,118000050 -6744,4,118000060 -6745,5,101000120 -6746,5,109000110 -6747,5,105000120 -6748,5,102000120 -6749,5,107000110 -6750,5,103000120 -6751,5,106000120 -6752,5,104000120 -6753,5,104000190 -6754,5,111000060 -6755,5,112000060 -6756,5,108000110 -6757,5,110000110 -6758,5,118000070 -6759,2,170003 -6760,3,170004 -6761,2,180003 -6762,3,180004 -6763,1,201000010 -6764,1,292000010 -6765,1,299000040 -6766,1,299000070 -6767,1,299000110 -6768,1,299000120 -6769,1,299000140 -6770,2,202000010 -6771,2,290000010 -6772,2,299000010 -6773,2,299000150 -6774,2,299000190 -6775,2,299000200 -6776,2,299000210 -6777,3,298000050 -6778,3,298000060 -6779,3,299000060 -6780,3,299000170 -6781,5,297000100 -6782,5,291000020 -6783,4,140000 -6784,5,150010 -6785,5,150020 -6786,5,150030 -6787,5,150040 -6789,3,101000050 -6790,3,101000060 -6791,3,101000080 -6792,3,101000040 -6793,3,109000060 -6794,3,109000070 -6795,3,109000080 -6796,3,105000060 -6797,3,105000070 -6798,3,105000080 -6799,3,104000050 -6800,3,106000050 -6801,3,102000060 -6802,3,102000070 -6803,3,102000080 -6804,3,103000050 -6805,3,105000050 -6806,3,107000060 -6807,3,107000070 -6808,3,107000080 -6809,3,108000050 -6810,3,109000050 -6811,3,103000060 -6812,3,103000070 -6813,3,103000080 -6814,3,110000050 -6815,3,106000060 -6816,3,106000070 -6817,3,106000080 -6818,3,101000070 -6819,3,110000060 -6820,3,104000060 -6821,3,104000070 -6822,3,104000080 -6823,3,102000050 -6824,3,104000170 -6825,3,104000260 -6826,3,111000010 -6827,3,111000020 -6828,3,111000030 -6829,3,112000020 -6830,3,112000030 -6831,3,108000060 -6832,3,108000070 -6833,3,108000080 -6834,3,107000050 -6835,3,112000010 -6836,3,110000070 -6837,3,110000080 -6838,3,118000020 -6839,3,118000030 -6840,3,118000040 -6841,4,101000090 -6842,4,101000100 -6843,4,101000110 -6844,4,109000100 -6845,4,105000100 -6846,4,105000110 -6847,4,108000090 -6848,4,110000090 -6849,4,102000100 -6850,4,102000110 -6851,4,106000090 -6852,4,109000090 -6853,4,107000100 -6854,4,103000090 -6855,4,102000090 -6856,4,103000100 -6857,4,106000100 -6858,4,106000110 -6859,4,104000090 -6860,4,104000100 -6861,4,104000110 -6862,4,107000090 -6863,4,104000180 -6864,4,111000040 -6865,4,112000040 -6866,4,108000100 -6867,4,105000090 -6868,4,110000100 -6869,4,118000050 -6870,4,118000060 -6871,5,101000120 -6872,5,109000110 -6873,5,105000120 -6874,5,102000120 -6875,5,107000110 -6876,5,103000120 -6877,5,106000120 -6878,5,104000120 -6879,5,104000190 -6880,5,111000060 -6881,5,112000060 -6882,5,108000110 -6883,5,110000110 -6884,5,118000070 -6885,3,170004 -6886,3,180004 -6887,1,201000010 -6888,1,292000010 -6889,1,299000040 -6890,1,299000070 -6891,1,299000110 -6892,1,299000120 -6893,1,299000140 -6894,2,202000010 -6895,2,290000010 -6896,2,299000010 -6897,2,299000150 -6898,2,299000190 -6899,2,299000200 -6900,2,299000210 -6901,3,298000050 -6902,3,298000060 -6903,3,299000060 -6904,3,299000170 -6905,5,297000100 -6906,5,291000020 -6907,4,140000 -6908,5,150010 -6909,5,150020 -6910,5,150030 -6911,5,150040 -6913,3,101000050 -6914,3,101000060 -6915,3,101000080 -6916,3,101000040 -6917,3,109000060 -6918,3,109000070 -6919,3,109000080 -6920,3,105000060 -6921,3,105000070 -6922,3,105000080 -6923,3,104000050 -6924,3,106000050 -6925,3,102000060 -6926,3,102000070 -6927,3,102000080 -6928,3,103000050 -6929,3,105000050 -6930,3,107000060 -6931,3,107000070 -6932,3,107000080 -6933,3,108000050 -6934,3,109000050 -6935,3,103000060 -6936,3,103000070 -6937,3,103000080 -6938,3,110000050 -6939,3,106000060 -6940,3,106000070 -6941,3,106000080 -6942,3,101000070 -6943,3,110000060 -6944,3,104000060 -6945,3,104000070 -6946,3,104000080 -6947,3,102000050 -6948,3,104000170 -6949,3,104000260 -6950,3,111000010 -6951,3,111000020 -6952,3,111000030 -6953,3,112000020 -6954,3,112000030 -6955,3,108000060 -6956,3,108000070 -6957,3,108000080 -6958,3,107000050 -6959,3,112000010 -6960,3,110000070 -6961,3,110000080 -6962,3,118000020 -6963,3,118000030 -6964,3,118000040 -6965,4,101000090 -6966,4,101000100 -6967,4,101000110 -6968,4,109000100 -6969,4,105000100 -6970,4,105000110 -6971,4,108000090 -6972,4,110000090 -6973,4,102000100 -6974,4,102000110 -6975,4,106000090 -6976,4,109000090 -6977,4,107000100 -6978,4,103000090 -6979,4,102000090 -6980,4,103000100 -6981,4,106000100 -6982,4,106000110 -6983,4,104000090 -6984,4,104000100 -6985,4,104000110 -6986,4,107000090 -6987,4,104000180 -6988,4,111000040 -6989,4,112000040 -6990,4,108000100 -6991,4,105000090 -6992,4,110000100 -6993,4,118000050 -6994,4,118000060 -6995,5,101000120 -6996,5,109000110 -6997,5,105000120 -6998,5,102000120 -6999,5,107000110 -7000,5,103000120 -7001,5,106000120 -7002,5,104000120 -7003,5,104000190 -7004,5,111000060 -7005,5,112000060 -7006,5,108000110 -7007,5,110000110 -7008,5,118000070 -7009,3,170004 -7010,3,180004 -7011,1,201000010 -7012,1,292000010 -7013,1,299000040 -7014,1,299000070 -7015,1,299000110 -7016,1,299000120 -7017,1,299000140 -7018,2,202000010 -7019,2,290000010 -7020,2,299000010 -7021,2,299000150 -7022,2,299000190 -7023,2,299000200 -7024,2,299000210 -7025,3,298000050 -7026,3,298000060 -7027,3,299000060 -7028,3,299000170 -7029,5,297000100 -7030,5,291000020 -7031,4,140000 -7032,5,150010 -7033,5,150020 -7034,5,150030 -7035,5,150040 -7037,3,101000050 -7038,3,101000060 -7039,3,101000080 -7040,3,101000040 -7041,3,109000060 -7042,3,109000070 -7043,3,109000080 -7044,3,105000060 -7045,3,105000070 -7046,3,105000080 -7047,3,104000050 -7048,3,106000050 -7049,3,102000060 -7050,3,102000070 -7051,3,102000080 -7052,3,103000050 -7053,3,105000050 -7054,3,107000060 -7055,3,107000070 -7056,3,107000080 -7057,3,108000050 -7058,3,109000050 -7059,3,103000060 -7060,3,103000070 -7061,3,103000080 -7062,3,110000050 -7063,3,106000060 -7064,3,106000070 -7065,3,106000080 -7066,3,101000070 -7067,3,110000060 -7068,3,104000060 -7069,3,104000070 -7070,3,104000080 -7071,3,102000050 -7072,3,104000170 -7073,3,104000260 -7074,3,111000010 -7075,3,111000020 -7076,3,111000030 -7077,3,112000020 -7078,3,112000030 -7079,3,108000060 -7080,3,108000070 -7081,3,108000080 -7082,3,107000050 -7083,3,112000010 -7084,3,110000070 -7085,3,110000080 -7086,3,118000020 -7087,3,118000030 -7088,3,118000040 -7089,4,101000090 -7090,4,101000100 -7091,4,101000110 -7092,4,109000100 -7093,4,105000100 -7094,4,105000110 -7095,4,108000090 -7096,4,110000090 -7097,4,102000100 -7098,4,102000110 -7099,4,106000090 -7100,4,109000090 -7101,4,107000100 -7102,4,103000090 -7103,4,102000090 -7104,4,103000100 -7105,4,106000100 -7106,4,106000110 -7107,4,104000090 -7108,4,104000100 -7109,4,104000110 -7110,4,107000090 -7111,4,104000180 -7112,4,111000040 -7113,4,112000040 -7114,4,108000100 -7115,4,105000090 -7116,4,110000100 -7117,4,118000050 -7118,4,118000060 -7119,5,101000120 -7120,5,109000110 -7121,5,105000120 -7122,5,102000120 -7123,5,107000110 -7124,5,103000120 -7125,5,106000120 -7126,5,104000120 -7127,5,104000190 -7128,5,111000060 -7129,5,112000060 -7130,5,108000110 -7131,5,110000110 -7132,5,118000070 -7133,3,170004 -7134,3,180004 -7135,1,201000010 -7136,1,292000010 -7137,1,299000040 -7138,1,299000070 -7139,1,299000110 -7140,1,299000120 -7141,1,299000140 -7142,2,202000010 -7143,2,290000010 -7144,2,299000010 -7145,2,299000150 -7146,2,299000190 -7147,2,299000200 -7148,2,299000210 -7149,3,298000050 -7150,3,298000060 -7151,3,299000060 -7152,3,299000170 -7153,5,297000100 -7154,5,291000020 -7155,4,140000 -7156,5,150010 -7157,5,150020 -7158,5,150030 -7159,5,150040 -7160,5,190000 -7161,5,200000 -7162,5,210000 -7164,3,118000020 -7165,3,118000030 -7166,3,118000040 -7167,3,106000060 -7168,3,106000070 -7169,3,106000080 -7170,3,101000070 -7171,3,110000060 -7172,3,104000060 -7173,3,104000070 -7174,3,104000080 -7175,3,102000050 -7176,3,104000170 -7177,3,104000260 -7178,4,118000050 -7179,4,118000060 -7180,4,106000100 -7181,4,106000110 -7182,4,104000090 -7183,4,104000100 -7184,4,104000110 -7185,4,104000270 -7186,4,107000090 -7187,4,104000180 -7188,5,118000070 -7189,5,106000120 -7190,5,104000120 -7191,5,104000190 -7192,1,103000000 -7193,2,103000001 -7194,3,103000002 -7195,4,103000003 -7196,5,103000004 -7197,1,111000000 -7198,2,111000001 -7199,3,111000002 -7200,4,111000003 -7201,5,111000004 -7202,1,115000000 -7203,2,115000001 -7204,3,115000002 -7205,4,115000003 -7206,5,115000004 -7207,3,170004 -7208,4,170005 -7209,3,180004 -7210,4,180005 -7211,4,140000 -7212,4,150010 -7213,4,150020 -7214,4,150030 -7215,4,150040 -7217,3,111000010 -7218,3,111000020 -7219,3,111000030 -7220,3,112000020 -7221,3,112000030 -7222,3,108000060 -7223,3,108000070 -7224,3,108000080 -7225,3,107000050 -7226,3,112000010 -7227,3,110000070 -7228,3,110000080 -7229,4,111000040 -7230,4,112000040 -7231,4,108000100 -7232,4,105000090 -7233,4,110000100 -7234,5,111000060 -7235,5,112000060 -7236,5,108000110 -7237,5,110000110 -7238,1,108000000 -7239,2,108000001 -7240,3,108000002 -7241,4,108000003 -7242,5,108000004 -7243,1,107000000 -7244,2,107000001 -7245,3,107000002 -7246,4,107000003 -7247,5,107000004 -7248,1,120000000 -7249,2,120000001 -7250,3,120000002 -7251,4,120000003 -7252,5,120000004 -7253,4,110024 -7254,4,110034 -7255,4,110044 -7256,4,110054 -7257,3,110060 -7258,3,110070 -7259,3,110080 -7260,3,110090 -7261,3,110100 -7262,3,110110 -7263,3,110120 -7264,3,110130 -7265,3,110140 -7266,3,110150 -7267,3,110160 -7268,3,110170 -7269,3,110180 -7270,3,110190 -7271,3,110200 -7272,3,110210 -7273,3,110220 -7274,3,110230 -7275,3,110240 -7276,3,110250 -7277,3,110260 -7278,3,110270 -7279,3,110620 -7280,3,110670 -7281,4,140000 -7282,4,150010 -7283,4,150020 -7284,4,150030 -7285,4,150040 -7287,3,101000050 -7288,3,101000060 -7289,3,101000080 -7290,3,101000040 -7291,3,109000060 -7292,3,109000070 -7293,3,109000080 -7294,3,105000060 -7295,3,105000070 -7296,3,105000080 -7297,3,104000050 -7298,3,106000050 -7299,4,101000090 -7300,4,101000100 -7301,4,101000110 -7302,4,109000100 -7303,4,105000100 -7304,4,105000110 -7305,4,108000090 -7306,4,110000090 -7307,5,101000120 -7308,5,109000110 -7309,5,105000120 -7310,1,101000000 -7311,2,101000001 -7312,3,101000002 -7313,4,101000008 -7314,5,101000004 -7315,1,109000000 -7316,2,109000001 -7317,3,109000002 -7318,4,109000003 -7319,5,109000004 -7320,4,120024 -7321,4,120034 -7322,4,120044 -7323,4,120054 -7324,3,120241 -7325,3,120251 -7326,3,120261 -7327,3,120271 -7328,3,120300 -7329,3,120310 -7330,3,120320 -7331,3,120330 -7332,3,120340 -7333,3,120350 -7334,3,120360 -7335,3,120370 -7336,3,120380 -7337,3,120390 -7338,3,120400 -7339,3,120410 -7340,3,120420 -7341,3,120430 -7342,3,120450 -7343,3,120460 -7344,3,120550 -7345,3,120560 -7346,3,120570 -7347,3,120990 -7348,3,121000 -7349,3,121010 -7350,3,121020 -7351,4,140000 -7352,4,150010 -7353,4,150020 -7354,4,150030 -7355,4,150040 -7357,3,102000060 -7358,3,102000070 -7359,3,102000080 -7360,3,103000050 -7361,3,105000050 -7362,3,107000060 -7363,3,107000070 -7364,3,107000080 -7365,3,108000050 -7366,3,109000050 -7367,3,103000060 -7368,3,103000070 -7369,3,103000080 -7370,3,110000050 -7371,4,102000100 -7372,4,102000110 -7373,4,106000090 -7374,4,109000090 -7375,4,107000100 -7376,4,103000090 -7377,4,102000090 -7378,4,103000100 -7379,5,102000120 -7380,5,107000110 -7381,5,103000120 -7382,1,102000000 -7383,2,102000001 -7384,3,102000002 -7385,4,102000003 -7386,5,102000004 -7387,1,105000000 -7388,2,105000001 -7389,3,105000002 -7390,4,105000003 -7391,5,105000004 -7392,1,112000000 -7393,2,112000001 -7394,3,112000002 -7395,4,112000003 -7396,5,112000004 -7397,4,130024 -7398,4,130034 -7399,4,130044 -7400,4,130054 -7401,3,130060 -7402,3,130070 -7403,3,130080 -7404,3,130090 -7405,3,130100 -7406,3,130110 -7407,3,130120 -7408,3,130130 -7409,3,130140 -7410,3,130150 -7411,3,130160 -7412,3,130170 -7413,3,130180 -7414,3,130190 -7415,3,130200 -7416,3,130420 -7417,3,130510 -7418,3,130520 -7419,3,130530 -7420,3,130540 -7421,3,130660 -7422,3,130790 -7423,3,130800 -7424,4,140000 -7425,4,150010 -7426,4,150020 -7427,4,150030 -7428,4,150040 -7430,1,101000010 -7431,1,102000010 -7432,1,103000010 -7433,1,104000010 -7434,1,105000010 -7435,1,106000010 -7436,1,107000010 -7437,1,108000010 -7438,1,109000010 -7439,1,110000010 -7440,2,101000020 -7441,2,101000030 -7442,2,102000020 -7443,2,102000030 -7444,2,102000040 -7445,2,103000020 -7446,2,103000030 -7447,2,103000040 -7448,2,104000020 -7449,2,104000030 -7450,2,104000040 -7451,2,105000020 -7452,2,105000030 -7453,2,105000040 -7454,2,106000020 -7455,2,106000030 -7456,2,106000040 -7457,2,107000020 -7458,2,107000030 -7459,2,107000040 -7460,2,108000020 -7461,2,108000030 -7462,2,108000040 -7463,2,109000020 -7464,2,109000030 -7465,2,109000040 -7466,2,110000020 -7467,2,110000030 -7468,2,110000040 -7469,2,118000010 -7470,3,101000050 -7471,3,101000060 -7472,3,101000080 -7473,3,101000040 -7474,3,109000060 -7475,3,109000070 -7476,3,109000080 -7477,3,105000060 -7478,3,105000070 -7479,3,105000080 -7480,3,104000050 -7481,3,106000050 -7482,3,102000060 -7483,3,102000070 -7484,3,102000080 -7485,3,103000050 -7486,3,105000050 -7487,3,107000060 -7488,3,107000070 -7489,3,107000080 -7490,3,108000050 -7491,3,109000050 -7492,3,103000060 -7493,3,103000070 -7494,3,103000080 -7495,3,110000050 -7496,3,106000060 -7497,3,106000070 -7498,3,106000080 -7499,3,101000070 -7500,3,110000060 -7501,3,104000060 -7502,3,104000070 -7503,3,104000080 -7504,3,102000050 -7505,3,104000170 -7506,3,104000260 -7507,3,111000010 -7508,3,111000020 -7509,3,111000030 -7510,3,112000020 -7511,3,112000030 -7512,3,108000060 -7513,3,108000070 -7514,3,108000080 -7515,3,107000050 -7516,3,112000010 -7517,3,110000070 -7518,3,110000080 -7519,3,118000020 -7520,3,118000030 -7521,3,118000040 -7522,4,101000090 -7523,4,101000100 -7524,4,101000110 -7525,4,109000100 -7526,4,105000100 -7527,4,105000110 -7528,4,108000090 -7529,4,110000090 -7530,4,102000100 -7531,4,102000110 -7532,4,106000090 -7533,4,109000090 -7534,4,107000100 -7535,4,103000090 -7536,4,102000090 -7537,4,103000100 -7538,4,106000100 -7539,4,106000110 -7540,4,104000090 -7541,4,104000100 -7542,4,104000110 -7543,4,107000090 -7544,4,104000180 -7545,4,111000040 -7546,4,112000040 -7547,4,108000100 -7548,4,105000090 -7549,4,110000100 -7550,4,118000050 -7551,4,118000060 -7552,5,101000120 -7553,5,109000110 -7554,5,105000120 -7555,5,102000120 -7556,5,107000110 -7557,5,103000120 -7558,5,106000120 -7559,5,104000120 -7560,5,104000190 -7561,5,111000060 -7562,5,112000060 -7563,5,108000110 -7564,5,110000110 -7565,5,118000070 -7566,1,170002 -7567,2,170003 -7568,3,170004 -7569,1,180002 -7570,2,180003 -7571,3,180004 -7572,1,201000010 -7573,1,292000010 -7574,1,299000040 -7575,1,299000070 -7576,1,299000110 -7577,1,299000120 -7578,1,299000140 -7579,2,202000010 -7580,2,290000010 -7581,2,299000010 -7582,2,299000150 -7583,2,299000190 -7584,2,299000200 -7585,2,299000210 -7586,3,298000050 -7587,3,298000060 -7588,3,299000060 -7589,3,299000170 -7590,3,290000120 -7591,3,291000050 -7592,3,292000020 -7593,4,299000670 -7594,4,299000680 -7595,4,204000010 -7596,5,297000100 -7597,5,291000020 -7598,5,297000130 -7599,5,297000140 -7600,5,203000010 -7601,4,140000 -7602,5,150010 -7603,5,150020 -7604,5,150030 -7605,5,150040 -7607,1,101000010 -7608,1,102000010 -7609,1,103000010 -7610,1,104000010 -7611,1,105000010 -7612,1,106000010 -7613,1,107000010 -7614,1,108000010 -7615,1,109000010 -7616,1,110000010 -7617,2,101000020 -7618,2,101000030 -7619,2,102000020 -7620,2,102000030 -7621,2,102000040 -7622,2,103000020 -7623,2,103000030 -7624,2,103000040 -7625,2,104000020 -7626,2,104000030 -7627,2,104000040 -7628,2,105000020 -7629,2,105000030 -7630,2,105000040 -7631,2,106000020 -7632,2,106000030 -7633,2,106000040 -7634,2,107000020 -7635,2,107000030 -7636,2,107000040 -7637,2,108000020 -7638,2,108000030 -7639,2,108000040 -7640,2,109000020 -7641,2,109000030 -7642,2,109000040 -7643,2,110000020 -7644,2,110000030 -7645,2,110000040 -7646,2,118000010 -7647,3,101000050 -7648,3,101000060 -7649,3,101000080 -7650,3,101000040 -7651,3,109000060 -7652,3,109000070 -7653,3,109000080 -7654,3,105000060 -7655,3,105000070 -7656,3,105000080 -7657,3,104000050 -7658,3,106000050 -7659,3,102000060 -7660,3,102000070 -7661,3,102000080 -7662,3,103000050 -7663,3,105000050 -7664,3,107000060 -7665,3,107000070 -7666,3,107000080 -7667,3,108000050 -7668,3,109000050 -7669,3,103000060 -7670,3,103000070 -7671,3,103000080 -7672,3,110000050 -7673,3,106000060 -7674,3,106000070 -7675,3,106000080 -7676,3,101000070 -7677,3,110000060 -7678,3,104000060 -7679,3,104000070 -7680,3,104000080 -7681,3,102000050 -7682,3,104000170 -7683,3,104000260 -7684,3,111000010 -7685,3,111000020 -7686,3,111000030 -7687,3,112000020 -7688,3,112000030 -7689,3,108000060 -7690,3,108000070 -7691,3,108000080 -7692,3,107000050 -7693,3,112000010 -7694,3,110000070 -7695,3,110000080 -7696,3,118000020 -7697,3,118000030 -7698,3,118000040 -7699,4,101000090 -7700,4,101000100 -7701,4,101000110 -7702,4,109000100 -7703,4,105000100 -7704,4,105000110 -7705,4,108000090 -7706,4,110000090 -7707,4,102000100 -7708,4,102000110 -7709,4,106000090 -7710,4,109000090 -7711,4,107000100 -7712,4,103000090 -7713,4,102000090 -7714,4,103000100 -7715,4,106000100 -7716,4,106000110 -7717,4,104000090 -7718,4,104000100 -7719,4,104000110 -7720,4,107000090 -7721,4,104000180 -7722,4,111000040 -7723,4,112000040 -7724,4,108000100 -7725,4,105000090 -7726,4,110000100 -7727,4,118000050 -7728,4,118000060 -7729,5,101000120 -7730,5,109000110 -7731,5,105000120 -7732,5,102000120 -7733,5,107000110 -7734,5,103000120 -7735,5,106000120 -7736,5,104000120 -7737,5,104000190 -7738,5,111000060 -7739,5,112000060 -7740,5,108000110 -7741,5,110000110 -7742,5,118000070 -7743,2,170003 -7744,3,170004 -7745,2,180003 -7746,3,180004 -7747,1,201000010 -7748,1,292000010 -7749,1,299000040 -7750,1,299000070 -7751,1,299000110 -7752,1,299000120 -7753,1,299000140 -7754,2,202000010 -7755,2,290000010 -7756,2,299000010 -7757,2,299000150 -7758,2,299000190 -7759,2,299000200 -7760,2,299000210 -7761,3,298000050 -7762,3,298000060 -7763,3,299000060 -7764,3,299000170 -7765,3,290000120 -7766,3,291000050 -7767,3,292000020 -7768,4,299000670 -7769,4,299000680 -7770,4,204000010 -7771,5,297000100 -7772,5,291000020 -7773,5,297000130 -7774,5,297000140 -7775,5,203000010 -7776,4,140000 -7777,5,150010 -7778,5,150020 -7779,5,150030 -7780,5,150040 -7782,3,101000050 -7783,3,101000060 -7784,3,101000080 -7785,3,101000040 -7786,3,109000060 -7787,3,109000070 -7788,3,109000080 -7789,3,105000060 -7790,3,105000070 -7791,3,105000080 -7792,3,104000050 -7793,3,106000050 -7794,3,102000060 -7795,3,102000070 -7796,3,102000080 -7797,3,103000050 -7798,3,105000050 -7799,3,107000060 -7800,3,107000070 -7801,3,107000080 -7802,3,108000050 -7803,3,109000050 -7804,3,103000060 -7805,3,103000070 -7806,3,103000080 -7807,3,110000050 -7808,3,106000060 -7809,3,106000070 -7810,3,106000080 -7811,3,101000070 -7812,3,110000060 -7813,3,104000060 -7814,3,104000070 -7815,3,104000080 -7816,3,102000050 -7817,3,104000170 -7818,3,104000260 -7819,3,111000010 -7820,3,111000020 -7821,3,111000030 -7822,3,112000020 -7823,3,112000030 -7824,3,108000060 -7825,3,108000070 -7826,3,108000080 -7827,3,107000050 -7828,3,112000010 -7829,3,110000070 -7830,3,110000080 -7831,3,118000020 -7832,3,118000030 -7833,3,118000040 -7834,4,101000090 -7835,4,101000100 -7836,4,101000110 -7837,4,109000100 -7838,4,105000100 -7839,4,105000110 -7840,4,108000090 -7841,4,110000090 -7842,4,102000100 -7843,4,102000110 -7844,4,106000090 -7845,4,109000090 -7846,4,107000100 -7847,4,103000090 -7848,4,102000090 -7849,4,103000100 -7850,4,106000100 -7851,4,106000110 -7852,4,104000090 -7853,4,104000100 -7854,4,104000110 -7855,4,107000090 -7856,4,104000180 -7857,4,111000040 -7858,4,112000040 -7859,4,108000100 -7860,4,105000090 -7861,4,110000100 -7862,4,118000050 -7863,4,118000060 -7864,5,101000120 -7865,5,109000110 -7866,5,105000120 -7867,5,102000120 -7868,5,107000110 -7869,5,103000120 -7870,5,106000120 -7871,5,104000120 -7872,5,104000190 -7873,5,111000060 -7874,5,112000060 -7875,5,108000110 -7876,5,110000110 -7877,5,118000070 -7878,3,170004 -7879,3,180004 -7880,1,201000010 -7881,1,292000010 -7882,1,299000040 -7883,1,299000070 -7884,1,299000110 -7885,1,299000120 -7886,1,299000140 -7887,2,202000010 -7888,2,290000010 -7889,2,299000010 -7890,2,299000150 -7891,2,299000190 -7892,2,299000200 -7893,2,299000210 -7894,3,298000050 -7895,3,298000060 -7896,3,299000060 -7897,3,299000170 -7898,3,290000120 -7899,3,291000050 -7900,3,292000020 -7901,4,299000670 -7902,4,299000680 -7903,4,204000010 -7904,5,297000100 -7905,5,291000020 -7906,5,297000130 -7907,5,297000140 -7908,5,203000010 -7909,4,140000 -7910,5,150010 -7911,5,150020 -7912,5,150030 -7913,5,150040 -7915,3,101000050 -7916,3,101000060 -7917,3,101000080 -7918,3,101000040 -7919,3,109000060 -7920,3,109000070 -7921,3,109000080 -7922,3,105000060 -7923,3,105000070 -7924,3,105000080 -7925,3,104000050 -7926,3,106000050 -7927,3,102000060 -7928,3,102000070 -7929,3,102000080 -7930,3,103000050 -7931,3,105000050 -7932,3,107000060 -7933,3,107000070 -7934,3,107000080 -7935,3,108000050 -7936,3,109000050 -7937,3,103000060 -7938,3,103000070 -7939,3,103000080 -7940,3,110000050 -7941,3,106000060 -7942,3,106000070 -7943,3,106000080 -7944,3,101000070 -7945,3,110000060 -7946,3,104000060 -7947,3,104000070 -7948,3,104000080 -7949,3,102000050 -7950,3,104000170 -7951,3,104000260 -7952,3,111000010 -7953,3,111000020 -7954,3,111000030 -7955,3,112000020 -7956,3,112000030 -7957,3,108000060 -7958,3,108000070 -7959,3,108000080 -7960,3,107000050 -7961,3,112000010 -7962,3,110000070 -7963,3,110000080 -7964,3,118000020 -7965,3,118000030 -7966,3,118000040 -7967,4,101000090 -7968,4,101000100 -7969,4,101000110 -7970,4,109000100 -7971,4,105000100 -7972,4,105000110 -7973,4,108000090 -7974,4,110000090 -7975,4,102000100 -7976,4,102000110 -7977,4,106000090 -7978,4,109000090 -7979,4,107000100 -7980,4,103000090 -7981,4,102000090 -7982,4,103000100 -7983,4,106000100 -7984,4,106000110 -7985,4,104000090 -7986,4,104000100 -7987,4,104000110 -7988,4,107000090 -7989,4,104000180 -7990,4,111000040 -7991,4,112000040 -7992,4,108000100 -7993,4,105000090 -7994,4,110000100 -7995,4,118000050 -7996,4,118000060 -7997,5,101000120 -7998,5,109000110 -7999,5,105000120 -8000,5,102000120 -8001,5,107000110 -8002,5,103000120 -8003,5,106000120 -8004,5,104000120 -8005,5,104000190 -8006,5,111000060 -8007,5,112000060 -8008,5,108000110 -8009,5,110000110 -8010,5,118000070 -8011,3,170004 -8012,3,180004 -8013,1,201000010 -8014,1,292000010 -8015,1,299000040 -8016,1,299000070 -8017,1,299000110 -8018,1,299000120 -8019,1,299000140 -8020,2,202000010 -8021,2,290000010 -8022,2,299000010 -8023,2,299000150 -8024,2,299000190 -8025,2,299000200 -8026,2,299000210 -8027,3,298000050 -8028,3,298000060 -8029,3,299000060 -8030,3,299000170 -8031,3,290000120 -8032,3,291000050 -8033,3,292000020 -8034,4,299000670 -8035,4,299000680 -8036,4,204000010 -8037,5,297000100 -8038,5,291000020 -8039,5,297000130 -8040,5,297000140 -8041,5,203000010 -8042,4,140000 -8043,5,150010 -8044,5,150020 -8045,5,150030 -8046,5,150040 -8048,3,101000050 -8049,3,101000060 -8050,3,101000080 -8051,3,101000040 -8052,3,109000060 -8053,3,109000070 -8054,3,109000080 -8055,3,105000060 -8056,3,105000070 -8057,3,105000080 -8058,3,104000050 -8059,3,106000050 -8060,3,102000060 -8061,3,102000070 -8062,3,102000080 -8063,3,103000050 -8064,3,105000050 -8065,3,107000060 -8066,3,107000070 -8067,3,107000080 -8068,3,108000050 -8069,3,109000050 -8070,3,103000060 -8071,3,103000070 -8072,3,103000080 -8073,3,110000050 -8074,3,106000060 -8075,3,106000070 -8076,3,106000080 -8077,3,101000070 -8078,3,110000060 -8079,3,104000060 -8080,3,104000070 -8081,3,104000080 -8082,3,102000050 -8083,3,104000170 -8084,3,104000260 -8085,3,111000010 -8086,3,111000020 -8087,3,111000030 -8088,3,112000020 -8089,3,112000030 -8090,3,108000060 -8091,3,108000070 -8092,3,108000080 -8093,3,107000050 -8094,3,112000010 -8095,3,110000070 -8096,3,110000080 -8097,3,118000020 -8098,3,118000030 -8099,3,118000040 -8100,4,101000090 -8101,4,101000100 -8102,4,101000110 -8103,4,109000100 -8104,4,105000100 -8105,4,105000110 -8106,4,108000090 -8107,4,110000090 -8108,4,102000100 -8109,4,102000110 -8110,4,106000090 -8111,4,109000090 -8112,4,107000100 -8113,4,103000090 -8114,4,102000090 -8115,4,103000100 -8116,4,106000100 -8117,4,106000110 -8118,4,104000090 -8119,4,104000100 -8120,4,104000110 -8121,4,107000090 -8122,4,104000180 -8123,4,111000040 -8124,4,112000040 -8125,4,108000100 -8126,4,105000090 -8127,4,110000100 -8128,4,118000050 -8129,4,118000060 -8130,5,101000120 -8131,5,109000110 -8132,5,105000120 -8133,5,102000120 -8134,5,107000110 -8135,5,103000120 -8136,5,106000120 -8137,5,104000120 -8138,5,104000190 -8139,5,111000060 -8140,5,112000060 -8141,5,108000110 -8142,5,110000110 -8143,5,118000070 -8144,3,170004 -8145,3,180004 -8146,1,201000010 -8147,1,292000010 -8148,1,299000040 -8149,1,299000070 -8150,1,299000110 -8151,1,299000120 -8152,1,299000140 -8153,2,202000010 -8154,2,290000010 -8155,2,299000010 -8156,2,299000150 -8157,2,299000190 -8158,2,299000200 -8159,2,299000210 -8160,3,298000050 -8161,3,298000060 -8162,3,299000060 -8163,3,299000170 -8164,3,290000120 -8165,3,291000050 -8166,3,292000020 -8167,4,299000670 -8168,4,299000680 -8169,4,204000010 -8170,5,297000100 -8171,5,291000020 -8172,5,297000130 -8173,5,297000140 -8174,5,203000010 -8175,4,140000 -8176,5,150010 -8177,5,150020 -8178,5,150030 -8179,5,150040 -8180,5,190000 -8181,5,200000 -8182,5,210000 -8184,3,102000060 -8185,3,102000070 -8186,3,102000080 -8187,3,103000050 -8188,3,105000050 -8189,3,107000060 -8190,3,107000070 -8191,3,107000080 -8192,3,108000050 -8193,3,109000050 -8194,3,103000060 -8195,3,103000070 -8196,3,103000080 -8197,3,110000050 -8198,4,102000100 -8199,4,102000110 -8200,4,106000090 -8201,4,109000090 -8202,4,107000100 -8203,4,103000090 -8204,4,102000090 -8205,4,103000100 -8206,5,102000120 -8207,5,107000110 -8208,5,103000120 -8209,1,102000000 -8210,2,102000001 -8211,3,102000002 -8212,4,102000003 -8213,5,102000004 -8214,1,105000000 -8215,2,105000001 -8216,3,105000002 -8217,4,105000003 -8218,5,105000004 -8219,1,112000000 -8220,2,112000001 -8221,3,112000002 -8222,4,112000003 -8223,5,112000004 -8224,3,170004 -8225,4,170005 -8226,3,180004 -8227,4,180005 -8228,4,140000 -8229,4,150010 -8230,4,150020 -8231,4,150030 -8232,4,150040 -8234,3,118000020 -8235,3,118000030 -8236,3,118000040 -8237,3,106000060 -8238,3,106000070 -8239,3,106000080 -8240,3,101000070 -8241,3,110000060 -8242,3,104000060 -8243,3,104000070 -8244,3,104000080 -8245,3,102000050 -8246,3,104000170 -8247,3,104000260 -8248,4,118000050 -8249,4,118000060 -8250,4,106000100 -8251,4,106000110 -8252,4,104000090 -8253,4,104000100 -8254,4,104000110 -8255,4,104000270 -8256,4,107000090 -8257,4,104000180 -8258,5,118000070 -8259,5,106000120 -8260,5,104000120 -8261,5,104000190 -8262,1,103000000 -8263,2,103000001 -8264,3,103000002 -8265,4,103000003 -8266,5,103000004 -8267,1,111000000 -8268,2,111000001 -8269,3,111000002 -8270,4,111000003 -8271,5,111000004 -8272,1,115000000 -8273,2,115000001 -8274,3,115000002 -8275,4,115000003 -8276,5,115000004 -8277,4,110024 -8278,4,110034 -8279,4,110044 -8280,4,110054 -8281,3,110060 -8282,3,110070 -8283,3,110080 -8284,3,110090 -8285,3,110100 -8286,3,110110 -8287,3,110120 -8288,3,110130 -8289,3,110140 -8290,3,110150 -8291,3,110160 -8292,3,110170 -8293,3,110180 -8294,3,110190 -8295,3,110200 -8296,3,110210 -8297,3,110220 -8298,3,110230 -8299,3,110240 -8300,3,110250 -8301,3,110260 -8302,3,110270 -8303,3,110620 -8304,3,110670 -8305,4,140000 -8306,4,150010 -8307,4,150020 -8308,4,150030 -8309,4,150040 -8311,3,111000010 -8312,3,111000020 -8313,3,111000030 -8314,3,112000020 -8315,3,112000030 -8316,3,108000060 -8317,3,108000070 -8318,3,108000080 -8319,3,107000050 -8320,3,112000010 -8321,3,110000070 -8322,3,110000080 -8323,4,111000040 -8324,4,112000040 -8325,4,108000100 -8326,4,105000090 -8327,4,110000100 -8328,5,111000060 -8329,5,112000060 -8330,5,108000110 -8331,5,110000110 -8332,1,108000000 -8333,2,108000001 -8334,3,108000002 -8335,4,108000003 -8336,5,108000004 -8337,1,107000000 -8338,2,107000001 -8339,3,107000002 -8340,4,107000003 -8341,5,107000004 -8342,1,120000000 -8343,2,120000001 -8344,3,120000002 -8345,4,120000003 -8346,5,120000004 -8347,4,120024 -8348,4,120034 -8349,4,120044 -8350,4,120054 -8351,3,120241 -8352,3,120251 -8353,3,120261 -8354,3,120271 -8355,3,120300 -8356,3,120310 -8357,3,120320 -8358,3,120330 -8359,3,120340 -8360,3,120350 -8361,3,120360 -8362,3,120370 -8363,3,120380 -8364,3,120390 -8365,3,120400 -8366,3,120410 -8367,3,120420 -8368,3,120430 -8369,3,120450 -8370,3,120460 -8371,3,120550 -8372,3,120560 -8373,3,120570 -8374,3,120990 -8375,3,121000 -8376,3,121010 -8377,3,121020 -8378,4,140000 -8379,4,150010 -8380,4,150020 -8381,4,150030 -8382,4,150040 -8384,3,101000050 -8385,3,101000060 -8386,3,101000080 -8387,3,101000040 -8388,3,109000060 -8389,3,109000070 -8390,3,109000080 -8391,3,105000060 -8392,3,105000070 -8393,3,105000080 -8394,3,104000050 -8395,3,106000050 -8396,4,101000090 -8397,4,101000100 -8398,4,101000110 -8399,4,109000100 -8400,4,105000100 -8401,4,105000110 -8402,4,108000090 -8403,4,110000090 -8404,5,101000120 -8405,5,109000110 -8406,5,105000120 -8407,1,101000000 -8408,2,101000001 -8409,3,101000002 -8410,4,101000008 -8411,5,101000004 -8412,1,109000000 -8413,2,109000001 -8414,3,109000002 -8415,4,109000003 -8416,5,109000004 -8417,4,130024 -8418,4,130034 -8419,4,130044 -8420,4,130054 -8421,3,130060 -8422,3,130070 -8423,3,130080 -8424,3,130090 -8425,3,130100 -8426,3,130110 -8427,3,130120 -8428,3,130130 -8429,3,130140 -8430,3,130150 -8431,3,130160 -8432,3,130170 -8433,3,130180 -8434,3,130190 -8435,3,130200 -8436,3,130420 -8437,3,130510 -8438,3,130520 -8439,3,130531 -8440,3,130540 -8441,3,130660 -8442,3,130790 -8443,3,130800 -8444,3,131130 -8445,4,140000 -8446,4,150010 -8447,4,150020 -8448,4,150030 -8449,4,150040 -8451,1,101000000 -8452,2,101000001 -8453,3,101000002 -8454,4,101000008 -8455,5,101000004 -8456,1,102000000 -8457,2,102000001 -8458,3,102000002 -8459,4,102000003 -8460,5,102000004 -8461,1,103000000 -8462,2,103000001 -8463,3,103000002 -8464,4,103000003 -8465,5,103000004 -8466,1,105000000 -8467,2,105000001 -8468,3,105000002 -8469,4,105000003 -8470,5,105000004 -8471,1,108000000 -8472,2,108000001 -8473,3,108000002 -8474,4,108000003 -8475,5,108000004 -8476,1,109000000 -8477,2,109000001 -8478,3,109000002 -8479,4,109000003 -8480,5,109000004 -8481,1,111000000 -8482,2,111000001 -8483,3,111000002 -8484,4,111000003 -8485,5,111000004 -8486,1,112000000 -8487,2,112000001 -8488,3,112000002 -8489,4,112000003 -8490,5,112000004 -8491,1,120000000 -8492,2,120000001 -8493,3,120000002 -8494,4,120000003 -8495,5,120000004 -8496,1,101000010 -8497,2,101000020 -8498,2,101000030 -8499,3,101000040 -8500,3,101000050 -8501,3,101000060 -8502,3,101000070 -8503,3,101000080 -8504,1,102000010 -8505,2,102000020 -8506,2,102000030 -8507,2,102000040 -8508,3,102000050 -8509,3,102000060 -8510,3,102000070 -8511,3,102000080 -8512,1,103000010 -8513,2,103000020 -8514,2,103000030 -8515,2,103000040 -8516,3,103000050 -8517,3,103000060 -8518,3,103000070 -8519,3,103000080 -8520,1,104000010 -8521,2,104000020 -8522,2,104000030 -8523,2,104000040 -8524,3,104000050 -8525,3,104000060 -8526,3,104000070 -8527,3,104000080 -8528,1,105000010 -8529,2,105000020 -8530,2,105000030 -8531,2,105000040 -8532,3,105000050 -8533,3,105000060 -8534,3,105000070 -8535,3,105000080 -8536,1,106000010 -8537,2,106000020 -8538,2,106000030 -8539,2,106000040 -8540,3,106000050 -8541,3,106000060 -8542,3,106000070 -8543,3,106000080 -8544,1,107000010 -8545,2,107000020 -8546,2,107000030 -8547,2,107000040 -8548,3,107000050 -8549,3,107000060 -8550,3,107000070 -8551,3,107000080 -8552,1,108000010 -8553,2,108000020 -8554,2,108000030 -8555,2,108000040 -8556,3,108000050 -8557,3,108000060 -8558,3,108000070 -8559,3,108000080 -8560,2,180000 -8561,2,170002 -8562,3,170003 -8563,4,170004 -8565,1,101000000 -8566,2,101000001 -8567,3,101000002 -8568,4,101000008 -8569,5,101000004 -8570,1,102000000 -8571,2,102000001 -8572,3,102000002 -8573,4,102000003 -8574,5,102000004 -8575,1,103000000 -8576,2,103000001 -8577,3,103000002 -8578,4,103000003 -8579,5,103000004 -8580,1,105000000 -8581,2,105000001 -8582,3,105000002 -8583,4,105000003 -8584,5,105000004 -8585,1,108000000 -8586,2,108000001 -8587,3,108000002 -8588,4,108000003 -8589,5,108000004 -8590,1,109000000 -8591,2,109000001 -8592,3,109000002 -8593,4,109000003 -8594,5,109000004 -8595,1,111000000 -8596,2,111000001 -8597,3,111000002 -8598,4,111000003 -8599,5,111000004 -8600,1,112000000 -8601,2,112000001 -8602,3,112000002 -8603,4,112000003 -8604,5,112000004 -8605,1,120000000 -8606,2,120000001 -8607,3,120000002 -8608,4,120000003 -8609,5,120000004 -8610,1,101000010 -8611,2,101000020 -8612,2,101000030 -8613,3,101000040 -8614,3,101000050 -8615,3,101000060 -8616,3,101000070 -8617,3,101000080 -8618,1,102000010 -8619,2,102000020 -8620,2,102000030 -8621,2,102000040 -8622,3,102000050 -8623,3,102000060 -8624,3,102000070 -8625,3,102000080 -8626,1,103000010 -8627,2,103000020 -8628,2,103000030 -8629,2,103000040 -8630,3,103000050 -8631,3,103000060 -8632,3,103000070 -8633,3,103000080 -8634,1,104000010 -8635,2,104000020 -8636,2,104000030 -8637,2,104000040 -8638,3,104000050 -8639,3,104000060 -8640,3,104000070 -8641,3,104000080 -8642,1,105000010 -8643,2,105000020 -8644,2,105000030 -8645,2,105000040 -8646,3,105000050 -8647,3,105000060 -8648,3,105000070 -8649,3,105000080 -8650,1,106000010 -8651,2,106000020 -8652,2,106000030 -8653,2,106000040 -8654,3,106000050 -8655,3,106000060 -8656,3,106000070 -8657,3,106000080 -8658,1,107000010 -8659,2,107000020 -8660,2,107000030 -8661,2,107000040 -8662,3,107000050 -8663,3,107000060 -8664,3,107000070 -8665,3,107000080 -8666,1,108000010 -8667,2,108000020 -8668,2,108000030 -8669,2,108000040 -8670,3,108000050 -8671,3,108000060 -8672,3,108000070 -8673,3,108000080 -8674,2,180000 -8675,2,170002 -8676,3,170003 -8677,4,170004 -8679,1,101000000 -8680,2,101000001 -8681,3,101000002 -8682,4,101000008 -8683,5,101000004 -8684,1,102000000 -8685,2,102000001 -8686,3,102000002 -8687,4,102000003 -8688,5,102000004 -8689,1,103000000 -8690,2,103000001 -8691,3,103000002 -8692,4,103000003 -8693,5,103000004 -8694,1,105000000 -8695,2,105000001 -8696,3,105000002 -8697,4,105000003 -8698,5,105000004 -8699,1,108000000 -8700,2,108000001 -8701,3,108000002 -8702,4,108000003 -8703,5,108000004 -8704,1,109000000 -8705,2,109000001 -8706,3,109000002 -8707,4,109000003 -8708,5,109000004 -8709,1,111000000 -8710,2,111000001 -8711,3,111000002 -8712,4,111000003 -8713,5,111000004 -8714,1,112000000 -8715,2,112000001 -8716,3,112000002 -8717,4,112000003 -8718,5,112000004 -8719,1,120000000 -8720,2,120000001 -8721,3,120000002 -8722,4,120000003 -8723,5,120000004 -8724,1,101000010 -8725,2,101000020 -8726,2,101000030 -8727,3,101000040 -8728,3,101000050 -8729,3,101000060 -8730,3,101000070 -8731,3,101000080 -8732,1,102000010 -8733,2,102000020 -8734,2,102000030 -8735,2,102000040 -8736,3,102000050 -8737,3,102000060 -8738,3,102000070 -8739,3,102000080 -8740,1,103000010 -8741,2,103000020 -8742,2,103000030 -8743,2,103000040 -8744,3,103000050 -8745,3,103000060 -8746,3,103000070 -8747,3,103000080 -8748,1,104000010 -8749,2,104000020 -8750,2,104000030 -8751,2,104000040 -8752,3,104000050 -8753,3,104000060 -8754,3,104000070 -8755,3,104000080 -8756,1,105000010 -8757,2,105000020 -8758,2,105000030 -8759,2,105000040 -8760,3,105000050 -8761,3,105000060 -8762,3,105000070 -8763,3,105000080 -8764,1,106000010 -8765,2,106000020 -8766,2,106000030 -8767,2,106000040 -8768,3,106000050 -8769,3,106000060 -8770,3,106000070 -8771,3,106000080 -8772,1,107000010 -8773,2,107000020 -8774,2,107000030 -8775,2,107000040 -8776,3,107000050 -8777,3,107000060 -8778,3,107000070 -8779,3,107000080 -8780,1,108000010 -8781,2,108000020 -8782,2,108000030 -8783,2,108000040 -8784,3,108000050 -8785,3,108000060 -8786,3,108000070 -8787,3,108000080 -8788,2,180001 -8789,2,170002 -8790,3,170003 -8791,4,170004 -8793,1,101000000 -8794,2,101000001 -8795,3,101000002 -8796,4,101000008 -8797,5,101000004 -8798,1,102000000 -8799,2,102000001 -8800,3,102000002 -8801,4,102000003 -8802,5,102000004 -8803,1,103000000 -8804,2,103000001 -8805,3,103000002 -8806,4,103000003 -8807,5,103000004 -8808,1,105000000 -8809,2,105000001 -8810,3,105000002 -8811,4,105000003 -8812,5,105000004 -8813,1,108000000 -8814,2,108000001 -8815,3,108000002 -8816,4,108000003 -8817,5,108000004 -8818,1,109000000 -8819,2,109000001 -8820,3,109000002 -8821,4,109000003 -8822,5,109000004 -8823,1,111000000 -8824,2,111000001 -8825,3,111000002 -8826,4,111000003 -8827,5,111000004 -8828,1,112000000 -8829,2,112000001 -8830,3,112000002 -8831,4,112000003 -8832,5,112000004 -8833,1,120000000 -8834,2,120000001 -8835,3,120000002 -8836,4,120000003 -8837,5,120000004 -8838,1,101000010 -8839,2,101000020 -8840,2,101000030 -8841,3,101000040 -8842,3,101000050 -8843,3,101000060 -8844,3,101000070 -8845,3,101000080 -8846,1,102000010 -8847,2,102000020 -8848,2,102000030 -8849,2,102000040 -8850,3,102000050 -8851,3,102000060 -8852,3,102000070 -8853,3,102000080 -8854,1,103000010 -8855,2,103000020 -8856,2,103000030 -8857,2,103000040 -8858,3,103000050 -8859,3,103000060 -8860,3,103000070 -8861,3,103000080 -8862,1,104000010 -8863,2,104000020 -8864,2,104000030 -8865,2,104000040 -8866,3,104000050 -8867,3,104000060 -8868,3,104000070 -8869,3,104000080 -8870,1,105000010 -8871,2,105000020 -8872,2,105000030 -8873,2,105000040 -8874,3,105000050 -8875,3,105000060 -8876,3,105000070 -8877,3,105000080 -8878,1,106000010 -8879,2,106000020 -8880,2,106000030 -8881,2,106000040 -8882,3,106000050 -8883,3,106000060 -8884,3,106000070 -8885,3,106000080 -8886,1,107000010 -8887,2,107000020 -8888,2,107000030 -8889,2,107000040 -8890,3,107000050 -8891,3,107000060 -8892,3,107000070 -8893,3,107000080 -8894,1,108000010 -8895,2,108000020 -8896,2,108000030 -8897,2,108000040 -8898,3,108000050 -8899,3,108000060 -8900,3,108000070 -8901,3,108000080 -8902,1,109000010 -8903,2,109000020 -8904,2,109000030 -8905,2,109000040 -8906,3,109000050 -8907,3,109000060 -8908,3,109000070 -8909,3,109000080 -8910,2,180001 -8911,2,170002 -8912,3,170003 -8913,4,170004 -8915,1,101000000 -8916,2,101000001 -8917,3,101000002 -8918,4,101000008 -8919,5,101000004 -8920,1,102000000 -8921,2,102000001 -8922,3,102000002 -8923,4,102000003 -8924,5,102000004 -8925,1,103000000 -8926,2,103000001 -8927,3,103000002 -8928,4,103000003 -8929,5,103000004 -8930,1,105000000 -8931,2,105000001 -8932,3,105000002 -8933,4,105000003 -8934,5,105000004 -8935,1,107000000 -8936,2,107000001 -8937,3,107000002 -8938,4,107000003 -8939,5,107000004 -8940,1,108000000 -8941,2,108000001 -8942,3,108000002 -8943,4,108000003 -8944,5,108000004 -8945,1,109000000 -8946,2,109000001 -8947,3,109000002 -8948,4,109000003 -8949,5,109000004 -8950,1,111000000 -8951,2,111000001 -8952,3,111000002 -8953,4,111000003 -8954,5,111000004 -8955,1,112000000 -8956,2,112000001 -8957,3,112000002 -8958,4,112000003 -8959,5,112000004 -8960,1,120000000 -8961,2,120000001 -8962,3,120000002 -8963,4,120000003 -8964,5,120000004 -8965,1,101000010 -8966,2,101000020 -8967,2,101000030 -8968,3,101000040 -8969,3,101000050 -8970,3,101000060 -8971,3,101000070 -8972,3,101000080 -8973,1,102000010 -8974,2,102000020 -8975,2,102000030 -8976,2,102000040 -8977,3,102000050 -8978,3,102000060 -8979,3,102000070 -8980,3,102000080 -8981,1,103000010 -8982,2,103000020 -8983,2,103000030 -8984,2,103000040 -8985,3,103000050 -8986,3,103000060 -8987,3,103000070 -8988,3,103000080 -8989,1,104000010 -8990,2,104000020 -8991,2,104000030 -8992,2,104000040 -8993,3,104000050 -8994,3,104000060 -8995,3,104000070 -8996,3,104000080 -8997,1,105000010 -8998,2,105000020 -8999,2,105000030 -9000,2,105000040 -9001,3,105000050 -9002,3,105000060 -9003,3,105000070 -9004,3,105000080 -9005,1,106000010 -9006,2,106000020 -9007,2,106000030 -9008,2,106000040 -9009,3,106000050 -9010,3,106000060 -9011,3,106000070 -9012,3,106000080 -9013,1,107000010 -9014,2,107000020 -9015,2,107000030 -9016,2,107000040 -9017,3,107000050 -9018,3,107000060 -9019,3,107000070 -9020,3,107000080 -9021,1,108000010 -9022,2,108000020 -9023,2,108000030 -9024,2,108000040 -9025,3,108000050 -9026,3,108000060 -9027,3,108000070 -9028,3,108000080 -9029,1,109000010 -9030,2,109000020 -9031,2,109000030 -9032,2,109000040 -9033,3,109000050 -9034,3,109000060 -9035,3,109000070 -9036,3,109000080 -9037,1,110000010 -9038,2,110000020 -9039,2,110000030 -9040,2,110000040 -9041,3,110000050 -9042,3,110000060 -9043,3,110000070 -9044,3,110000080 -9045,2,180001 -9046,2,170002 -9047,3,170003 -9048,4,170004 -9050,1,101000010 -9051,1,102000010 -9052,1,103000010 -9053,1,104000010 -9054,1,105000010 -9055,1,106000010 -9056,1,107000010 -9057,1,108000010 -9058,1,109000010 -9059,1,110000010 -9060,2,101000020 -9061,2,101000030 -9062,2,102000020 -9063,2,102000030 -9064,2,102000040 -9065,2,103000020 -9066,2,103000030 -9067,2,103000040 -9068,2,104000020 -9069,2,104000030 -9070,2,104000040 -9071,2,105000020 -9072,2,105000030 -9073,2,105000040 -9074,2,106000020 -9075,2,106000030 -9076,2,106000040 -9077,2,107000020 -9078,2,107000030 -9079,2,107000040 -9080,2,108000020 -9081,2,108000030 -9082,2,108000040 -9083,2,109000020 -9084,2,109000030 -9085,2,109000040 -9086,2,110000020 -9087,2,110000030 -9088,2,110000040 -9089,2,118000010 -9090,3,101000050 -9091,3,101000060 -9092,3,101000080 -9093,3,101000040 -9094,3,109000060 -9095,3,109000070 -9096,3,109000080 -9097,3,105000060 -9098,3,105000070 -9099,3,105000080 -9100,3,104000050 -9101,3,106000050 -9102,3,102000060 -9103,3,102000070 -9104,3,102000080 -9105,3,103000050 -9106,3,105000050 -9107,3,107000060 -9108,3,107000070 -9109,3,107000080 -9110,3,108000050 -9111,3,109000050 -9112,3,103000060 -9113,3,103000070 -9114,3,103000080 -9115,3,110000050 -9116,3,106000060 -9117,3,106000070 -9118,3,106000080 -9119,3,101000070 -9120,3,110000060 -9121,3,104000060 -9122,3,104000070 -9123,3,104000080 -9124,3,102000050 -9125,3,104000170 -9126,3,104000260 -9127,3,111000010 -9128,3,111000020 -9129,3,111000030 -9130,3,112000020 -9131,3,112000030 -9132,3,108000060 -9133,3,108000070 -9134,3,108000080 -9135,3,107000050 -9136,3,112000010 -9137,3,110000070 -9138,3,110000080 -9139,3,118000020 -9140,3,118000030 -9141,3,118000040 -9142,4,101000090 -9143,4,101000100 -9144,4,101000110 -9145,4,109000100 -9146,4,105000100 -9147,4,105000110 -9148,4,108000090 -9149,4,110000090 -9150,4,102000100 -9151,4,102000110 -9152,4,106000090 -9153,4,109000090 -9154,4,107000100 -9155,4,103000090 -9156,4,102000090 -9157,4,103000100 -9158,4,106000100 -9159,4,106000110 -9160,4,104000090 -9161,4,104000100 -9162,4,104000110 -9163,4,107000090 -9164,4,104000180 -9165,4,111000040 -9166,4,112000040 -9167,4,108000100 -9168,4,105000090 -9169,4,110000100 -9170,4,118000050 -9171,4,118000060 -9172,5,101000120 -9173,5,109000110 -9174,5,105000120 -9175,5,102000120 -9176,5,107000110 -9177,5,103000120 -9178,5,106000120 -9179,5,104000120 -9180,5,104000190 -9181,5,111000060 -9182,5,112000060 -9183,5,108000110 -9184,5,110000110 -9185,5,118000070 -9186,1,201000010 -9187,1,292000010 -9188,1,299000040 -9189,1,299000070 -9190,1,299000110 -9191,1,299000120 -9192,1,299000140 -9193,2,202000010 -9194,2,290000010 -9195,2,299000010 -9196,2,299000150 -9197,2,299000190 -9198,2,299000200 -9199,2,299000210 -9200,3,298000050 -9201,3,298000060 -9202,3,299000060 -9203,3,299000170 -9204,3,290000120 -9205,3,291000050 -9206,3,292000020 -9207,4,299000670 -9208,4,299000680 -9209,4,204000010 -9210,4,209000040 -9211,5,297000100 -9212,5,291000020 -9213,5,297000130 -9214,5,297000140 -9215,5,203000010 -9216,5,206000030 -9217,1,170002 -9218,1,180002 -9219,2,170003 -9220,2,180003 -9221,3,170004 -9222,3,180004 -9223,4,140000 -9224,5,150010 -9225,5,150020 -9226,5,150030 -9227,5,150040 -9229,1,101000010 -9230,1,102000010 -9231,1,103000010 -9232,1,104000010 -9233,1,105000010 -9234,1,106000010 -9235,1,107000010 -9236,1,108000010 -9237,1,109000010 -9238,1,110000010 -9239,2,101000020 -9240,2,101000030 -9241,2,102000020 -9242,2,102000030 -9243,2,102000040 -9244,2,103000020 -9245,2,103000030 -9246,2,103000040 -9247,2,104000020 -9248,2,104000030 -9249,2,104000040 -9250,2,105000020 -9251,2,105000030 -9252,2,105000040 -9253,2,106000020 -9254,2,106000030 -9255,2,106000040 -9256,2,107000020 -9257,2,107000030 -9258,2,107000040 -9259,2,108000020 -9260,2,108000030 -9261,2,108000040 -9262,2,109000020 -9263,2,109000030 -9264,2,109000040 -9265,2,110000020 -9266,2,110000030 -9267,2,110000040 -9268,2,118000010 -9269,3,101000050 -9270,3,101000060 -9271,3,101000080 -9272,3,101000040 -9273,3,109000060 -9274,3,109000070 -9275,3,109000080 -9276,3,105000060 -9277,3,105000070 -9278,3,105000080 -9279,3,104000050 -9280,3,106000050 -9281,3,102000060 -9282,3,102000070 -9283,3,102000080 -9284,3,103000050 -9285,3,105000050 -9286,3,107000060 -9287,3,107000070 -9288,3,107000080 -9289,3,108000050 -9290,3,109000050 -9291,3,103000060 -9292,3,103000070 -9293,3,103000080 -9294,3,110000050 -9295,3,106000060 -9296,3,106000070 -9297,3,106000080 -9298,3,101000070 -9299,3,110000060 -9300,3,104000060 -9301,3,104000070 -9302,3,104000080 -9303,3,102000050 -9304,3,104000170 -9305,3,104000260 -9306,3,111000010 -9307,3,111000020 -9308,3,111000030 -9309,3,112000020 -9310,3,112000030 -9311,3,108000060 -9312,3,108000070 -9313,3,108000080 -9314,3,107000050 -9315,3,112000010 -9316,3,110000070 -9317,3,110000080 -9318,3,118000020 -9319,3,118000030 -9320,3,118000040 -9321,4,101000090 -9322,4,101000100 -9323,4,101000110 -9324,4,109000100 -9325,4,105000100 -9326,4,105000110 -9327,4,108000090 -9328,4,110000090 -9329,4,102000100 -9330,4,102000110 -9331,4,106000090 -9332,4,109000090 -9333,4,107000100 -9334,4,103000090 -9335,4,102000090 -9336,4,103000100 -9337,4,106000100 -9338,4,106000110 -9339,4,104000090 -9340,4,104000100 -9341,4,104000110 -9342,4,107000090 -9343,4,104000180 -9344,4,111000040 -9345,4,112000040 -9346,4,108000100 -9347,4,105000090 -9348,4,110000100 -9349,4,118000050 -9350,4,118000060 -9351,5,101000120 -9352,5,109000110 -9353,5,105000120 -9354,5,102000120 -9355,5,107000110 -9356,5,103000120 -9357,5,106000120 -9358,5,104000120 -9359,5,104000190 -9360,5,111000060 -9361,5,112000060 -9362,5,108000110 -9363,5,110000110 -9364,5,118000070 -9365,1,201000010 -9366,1,292000010 -9367,1,299000040 -9368,1,299000070 -9369,1,299000110 -9370,1,299000120 -9371,1,299000140 -9372,2,202000010 -9373,2,290000010 -9374,2,299000010 -9375,2,299000150 -9376,2,299000190 -9377,2,299000200 -9378,2,299000210 -9379,3,298000050 -9380,3,298000060 -9381,3,299000060 -9382,3,299000170 -9383,3,290000120 -9384,3,291000050 -9385,3,292000020 -9386,4,299000670 -9387,4,299000680 -9388,4,204000010 -9389,4,209000040 -9390,5,297000100 -9391,5,291000020 -9392,5,297000130 -9393,5,297000140 -9394,5,203000010 -9395,5,206000030 -9396,2,170003 -9397,2,180003 -9398,3,170004 -9399,3,180004 -9400,4,140000 -9401,5,150010 -9402,5,150020 -9403,5,150030 -9404,5,150040 -9406,3,101000050 -9407,3,101000060 -9408,3,101000080 -9409,3,101000040 -9410,3,109000060 -9411,3,109000070 -9412,3,109000080 -9413,3,105000060 -9414,3,105000070 -9415,3,105000080 -9416,3,104000050 -9417,3,106000050 -9418,3,102000060 -9419,3,102000070 -9420,3,102000080 -9421,3,103000050 -9422,3,105000050 -9423,3,107000060 -9424,3,107000070 -9425,3,107000080 -9426,3,108000050 -9427,3,109000050 -9428,3,103000060 -9429,3,103000070 -9430,3,103000080 -9431,3,110000050 -9432,3,106000060 -9433,3,106000070 -9434,3,106000080 -9435,3,101000070 -9436,3,110000060 -9437,3,104000060 -9438,3,104000070 -9439,3,104000080 -9440,3,102000050 -9441,3,104000170 -9442,3,104000260 -9443,3,111000010 -9444,3,111000020 -9445,3,111000030 -9446,3,112000020 -9447,3,112000030 -9448,3,108000060 -9449,3,108000070 -9450,3,108000080 -9451,3,107000050 -9452,3,112000010 -9453,3,110000070 -9454,3,110000080 -9455,3,118000020 -9456,3,118000030 -9457,3,118000040 -9458,4,101000090 -9459,4,101000100 -9460,4,101000110 -9461,4,109000100 -9462,4,105000100 -9463,4,105000110 -9464,4,108000090 -9465,4,110000090 -9466,4,102000100 -9467,4,102000110 -9468,4,106000090 -9469,4,109000090 -9470,4,107000100 -9471,4,103000090 -9472,4,102000090 -9473,4,103000100 -9474,4,106000100 -9475,4,106000110 -9476,4,104000090 -9477,4,104000100 -9478,4,104000110 -9479,4,107000090 -9480,4,104000180 -9481,4,111000040 -9482,4,112000040 -9483,4,108000100 -9484,4,105000090 -9485,4,110000100 -9486,4,118000050 -9487,4,118000060 -9488,5,101000120 -9489,5,109000110 -9490,5,105000120 -9491,5,102000120 -9492,5,107000110 -9493,5,103000120 -9494,5,106000120 -9495,5,104000120 -9496,5,104000190 -9497,5,111000060 -9498,5,112000060 -9499,5,108000110 -9500,5,110000110 -9501,5,118000070 -9502,1,201000010 -9503,1,292000010 -9504,1,299000040 -9505,1,299000070 -9506,1,299000110 -9507,1,299000120 -9508,1,299000140 -9509,2,202000010 -9510,2,290000010 -9511,2,299000010 -9512,2,299000150 -9513,2,299000190 -9514,2,299000200 -9515,2,299000210 -9516,3,298000050 -9517,3,298000060 -9518,3,299000060 -9519,3,299000170 -9520,3,290000120 -9521,3,291000050 -9522,3,292000020 -9523,4,299000670 -9524,4,299000680 -9525,4,204000010 -9526,4,209000040 -9527,5,297000100 -9528,5,291000020 -9529,5,297000130 -9530,5,297000140 -9531,5,203000010 -9532,5,206000030 -9533,3,170004 -9534,3,180004 -9535,4,140000 -9536,5,150010 -9537,5,150020 -9538,5,150030 -9539,5,150040 -9541,3,101000050 -9542,3,101000060 -9543,3,101000080 -9544,3,101000040 -9545,3,109000060 -9546,3,109000070 -9547,3,109000080 -9548,3,105000060 -9549,3,105000070 -9550,3,105000080 -9551,3,104000050 -9552,3,106000050 -9553,3,102000060 -9554,3,102000070 -9555,3,102000080 -9556,3,103000050 -9557,3,105000050 -9558,3,107000060 -9559,3,107000070 -9560,3,107000080 -9561,3,108000050 -9562,3,109000050 -9563,3,103000060 -9564,3,103000070 -9565,3,103000080 -9566,3,110000050 -9567,3,106000060 -9568,3,106000070 -9569,3,106000080 -9570,3,101000070 -9571,3,110000060 -9572,3,104000060 -9573,3,104000070 -9574,3,104000080 -9575,3,102000050 -9576,3,104000170 -9577,3,104000260 -9578,3,111000010 -9579,3,111000020 -9580,3,111000030 -9581,3,112000020 -9582,3,112000030 -9583,3,108000060 -9584,3,108000070 -9585,3,108000080 -9586,3,107000050 -9587,3,112000010 -9588,3,110000070 -9589,3,110000080 -9590,3,118000020 -9591,3,118000030 -9592,3,118000040 -9593,4,101000090 -9594,4,101000100 -9595,4,101000110 -9596,4,109000100 -9597,4,105000100 -9598,4,105000110 -9599,4,108000090 -9600,4,110000090 -9601,4,102000100 -9602,4,102000110 -9603,4,106000090 -9604,4,109000090 -9605,4,107000100 -9606,4,103000090 -9607,4,102000090 -9608,4,103000100 -9609,4,106000100 -9610,4,106000110 -9611,4,104000090 -9612,4,104000100 -9613,4,104000110 -9614,4,107000090 -9615,4,104000180 -9616,4,111000040 -9617,4,112000040 -9618,4,108000100 -9619,4,105000090 -9620,4,110000100 -9621,4,118000050 -9622,4,118000060 -9623,5,101000120 -9624,5,109000110 -9625,5,105000120 -9626,5,102000120 -9627,5,107000110 -9628,5,103000120 -9629,5,106000120 -9630,5,104000120 -9631,5,104000190 -9632,5,111000060 -9633,5,112000060 -9634,5,108000110 -9635,5,110000110 -9636,5,118000070 -9637,1,201000010 -9638,1,292000010 -9639,1,299000040 -9640,1,299000070 -9641,1,299000110 -9642,1,299000120 -9643,1,299000140 -9644,2,202000010 -9645,2,290000010 -9646,2,299000010 -9647,2,299000150 -9648,2,299000190 -9649,2,299000200 -9650,2,299000210 -9651,3,298000050 -9652,3,298000060 -9653,3,299000060 -9654,3,299000170 -9655,3,290000120 -9656,3,291000050 -9657,3,292000020 -9658,4,299000670 -9659,4,299000680 -9660,4,204000010 -9661,4,209000040 -9662,5,297000100 -9663,5,291000020 -9664,5,297000130 -9665,5,297000140 -9666,5,203000010 -9667,5,206000030 -9668,3,170004 -9669,3,180004 -9670,4,140000 -9671,5,150010 -9672,5,150020 -9673,5,150030 -9674,5,150040 -9676,3,101000050 -9677,3,101000060 -9678,3,101000080 -9679,3,101000040 -9680,3,109000060 -9681,3,109000070 -9682,3,109000080 -9683,3,105000060 -9684,3,105000070 -9685,3,105000080 -9686,3,104000050 -9687,3,106000050 -9688,3,102000060 -9689,3,102000070 -9690,3,102000080 -9691,3,103000050 -9692,3,105000050 -9693,3,107000060 -9694,3,107000070 -9695,3,107000080 -9696,3,108000050 -9697,3,109000050 -9698,3,103000060 -9699,3,103000070 -9700,3,103000080 -9701,3,110000050 -9702,3,106000060 -9703,3,106000070 -9704,3,106000080 -9705,3,101000070 -9706,3,110000060 -9707,3,104000060 -9708,3,104000070 -9709,3,104000080 -9710,3,102000050 -9711,3,104000170 -9712,3,104000260 -9713,3,111000010 -9714,3,111000020 -9715,3,111000030 -9716,3,112000020 -9717,3,112000030 -9718,3,108000060 -9719,3,108000070 -9720,3,108000080 -9721,3,107000050 -9722,3,112000010 -9723,3,110000070 -9724,3,110000080 -9725,3,118000020 -9726,3,118000030 -9727,3,118000040 -9728,4,101000090 -9729,4,101000100 -9730,4,101000110 -9731,4,109000100 -9732,4,105000100 -9733,4,105000110 -9734,4,108000090 -9735,4,110000090 -9736,4,102000100 -9737,4,102000110 -9738,4,106000090 -9739,4,109000090 -9740,4,107000100 -9741,4,103000090 -9742,4,102000090 -9743,4,103000100 -9744,4,106000100 -9745,4,106000110 -9746,4,104000090 -9747,4,104000100 -9748,4,104000110 -9749,4,107000090 -9750,4,104000180 -9751,4,111000040 -9752,4,112000040 -9753,4,108000100 -9754,4,105000090 -9755,4,110000100 -9756,4,118000050 -9757,4,118000060 -9758,5,101000120 -9759,5,109000110 -9760,5,105000120 -9761,5,102000120 -9762,5,107000110 -9763,5,103000120 -9764,5,106000120 -9765,5,104000120 -9766,5,104000190 -9767,5,111000060 -9768,5,112000060 -9769,5,108000110 -9770,5,110000110 -9771,5,118000070 -9772,1,201000010 -9773,1,292000010 -9774,1,299000040 -9775,1,299000070 -9776,1,299000110 -9777,1,299000120 -9778,1,299000140 -9779,2,202000010 -9780,2,290000010 -9781,2,299000010 -9782,2,299000150 -9783,2,299000190 -9784,2,299000200 -9785,2,299000210 -9786,3,298000050 -9787,3,298000060 -9788,3,299000060 -9789,3,299000170 -9790,3,290000120 -9791,3,291000050 -9792,3,292000020 -9793,4,299000670 -9794,4,299000680 -9795,4,204000010 -9796,4,209000040 -9797,5,297000100 -9798,5,291000020 -9799,5,297000130 -9800,5,297000140 -9801,5,203000010 -9802,5,206000030 -9803,3,170004 -9804,3,180004 -9805,4,140000 -9806,5,150010 -9807,5,150020 -9808,5,150030 -9809,5,150040 -9810,5,190000 -9811,5,200000 -9812,5,210000 -9814,3,101000050 -9815,3,101000060 -9816,3,101000080 -9817,3,101000040 -9818,3,109000060 -9819,3,109000070 -9820,3,109000080 -9821,3,105000060 -9822,3,105000070 -9823,3,105000080 -9824,3,104000050 -9825,3,106000050 -9826,4,101000090 -9827,4,101000100 -9828,4,101000110 -9829,4,109000100 -9830,4,105000100 -9831,4,105000110 -9832,4,108000090 -9833,4,110000090 -9834,5,101000120 -9835,5,109000110 -9836,5,105000120 -9837,1,101000000 -9838,2,101000001 -9839,3,101000002 -9840,4,101000008 -9841,5,101000004 -9842,1,109000000 -9843,2,109000001 -9844,3,109000002 -9845,4,109000003 -9846,5,109000004 -9847,3,170004 -9848,4,170005 -9849,3,180004 -9850,4,180005 -9851,4,140000 -9852,4,150010 -9853,4,150020 -9854,4,150030 -9855,4,150040 -9857,3,102000060 -9858,3,102000070 -9859,3,102000080 -9860,3,103000050 -9861,3,105000050 -9862,3,107000060 -9863,3,107000070 -9864,3,107000080 -9865,3,108000050 -9866,3,109000050 -9867,3,103000060 -9868,3,103000070 -9869,3,103000080 -9870,3,110000050 -9871,4,102000100 -9872,4,102000350 -9873,4,102000110 -9874,4,106000090 -9875,4,109000090 -9876,4,107000100 -9877,4,103000090 -9878,4,102000090 -9879,4,103000100 -9880,5,102000120 -9881,5,107000110 -9882,5,103000120 -9883,1,102000000 -9884,2,102000001 -9885,3,102000002 -9886,4,102000003 -9887,5,102000004 -9888,1,105000000 -9889,2,105000001 -9890,3,105000002 -9891,4,105000003 -9892,5,105000004 -9893,1,112000000 -9894,2,112000001 -9895,3,112000002 -9896,4,112000003 -9897,5,112000004 -9898,4,110024 -9899,4,110034 -9900,4,110044 -9901,4,110054 -9902,3,110060 -9903,3,110070 -9904,3,110080 -9905,3,110090 -9906,3,110100 -9907,3,110110 -9908,3,110120 -9909,3,110130 -9910,3,110140 -9911,3,110150 -9912,3,110160 -9913,3,110170 -9914,3,110180 -9915,3,110190 -9916,3,110200 -9917,3,110210 -9918,3,110220 -9919,3,110230 -9920,3,110240 -9921,3,110250 -9922,3,110260 -9923,3,110270 -9924,3,110620 -9925,3,110670 -9926,4,140000 -9927,4,150010 -9928,4,150020 -9929,4,150030 -9930,4,150040 -9932,3,118000020 -9933,3,118000030 -9934,3,118000040 -9935,3,106000060 -9936,3,106000070 -9937,3,106000080 -9938,3,101000070 -9939,3,110000060 -9940,3,104000060 -9941,3,104000070 -9942,3,104000080 -9943,3,102000050 -9944,3,104000170 -9945,3,104000260 -9946,4,118000050 -9947,4,118000060 -9948,4,106000100 -9949,4,106000110 -9950,4,104000090 -9951,4,104000100 -9952,4,104000110 -9953,4,104000270 -9954,4,107000090 -9955,4,104000180 -9956,5,118000070 -9957,5,106000120 -9958,5,104000120 -9959,5,104000190 -9960,1,103000000 -9961,2,103000001 -9962,3,103000002 -9963,4,103000003 -9964,5,103000004 -9965,1,111000000 -9966,2,111000001 -9967,3,111000002 -9968,4,111000003 -9969,5,111000004 -9970,1,115000000 -9971,2,115000001 -9972,3,115000002 -9973,4,115000003 -9974,5,115000004 -9975,4,120024 -9976,4,120034 -9977,4,120044 -9978,4,120054 -9979,3,120241 -9980,3,120251 -9981,3,120261 -9982,3,120271 -9983,3,120300 -9984,3,120310 -9985,3,120320 -9986,3,120330 -9987,3,120340 -9988,3,120350 -9989,3,120360 -9990,3,120370 -9991,3,120380 -9992,3,120390 -9993,3,120400 -9994,3,120410 -9995,3,120420 -9996,3,120430 -9997,3,120450 -9998,3,120460 -9999,3,120550 -10000,3,120560 -10001,3,120570 -10002,3,120990 -10003,3,121000 -10004,3,121010 -10005,3,121020 -10006,4,140000 -10007,4,150010 -10008,4,150020 -10009,4,150030 -10010,4,150040 -10012,3,111000010 -10013,3,111000020 -10014,3,111000030 -10015,3,112000020 -10016,3,112000030 -10017,3,108000060 -10018,3,108000070 -10019,3,108000080 -10020,3,107000050 -10021,3,112000010 -10022,3,110000070 -10023,3,110000080 -10024,4,111000040 -10025,4,112000040 -10026,4,108000100 -10027,4,105000090 -10028,4,110000100 -10029,5,111000060 -10030,5,112000060 -10031,5,108000110 -10032,5,110000110 -10033,1,108000000 -10034,2,108000001 -10035,3,108000002 -10036,4,108000003 -10037,5,108000004 -10038,1,107000000 -10039,2,107000001 -10040,3,107000002 -10041,4,107000003 -10042,5,107000004 -10043,1,120000000 -10044,2,120000001 -10045,3,120000002 -10046,4,120000003 -10047,5,120000004 -10048,4,130024 -10049,4,130034 -10050,4,130044 -10051,4,130054 -10052,3,130060 -10053,3,130070 -10054,3,130080 -10055,3,130090 -10056,3,130100 -10057,3,130110 -10058,3,130120 -10059,3,130130 -10060,3,130140 -10061,3,130150 -10062,3,130160 -10063,3,130170 -10064,3,130180 -10065,3,130190 -10066,3,130200 -10067,3,130420 -10068,3,130510 -10069,3,130520 -10070,3,130531 -10071,3,130540 -10072,3,130660 -10073,3,130790 -10074,3,130800 -10075,3,131130 -10076,4,140000 -10077,4,150010 -10078,4,150020 -10079,4,150030 -10080,4,150040 -10082,3,111000010 -10083,3,111000020 -10084,3,111000030 -10085,3,112000020 -10086,3,112000030 -10087,3,108000060 -10088,3,108000070 -10089,3,108000080 -10090,3,107000050 -10091,3,112000010 -10092,3,110000070 -10093,3,110000080 -10094,4,111000040 -10095,4,112000040 -10096,4,108000100 -10097,4,105000090 -10098,4,110000100 -10099,5,111000060 -10100,5,112000060 -10101,5,108000110 -10102,5,110000110 -10103,1,108000000 -10104,2,108000001 -10105,3,108000002 -10106,4,108000003 -10107,5,108000004 -10108,1,107000000 -10109,2,107000001 -10110,3,107000002 -10111,4,107000003 -10112,5,107000004 -10113,1,120000000 -10114,2,120000001 -10115,3,120000002 -10116,4,120000003 -10117,5,120000004 -10118,3,170004 -10119,4,170005 -10120,3,180004 -10121,4,180005 -10122,4,140000 -10123,4,150010 -10124,4,150020 -10125,4,150030 -10126,4,150040 -10128,3,101000050 -10129,3,101000060 -10130,3,101000080 -10131,3,101000040 -10132,3,109000060 -10133,3,109000070 -10134,3,109000080 -10135,3,105000060 -10136,3,105000070 -10137,3,105000080 -10138,3,104000050 -10139,3,106000050 -10140,4,101000090 -10141,4,101000100 -10142,4,101000110 -10143,4,109000100 -10144,4,105000100 -10145,4,105000110 -10146,4,108000090 -10147,4,110000090 -10148,5,101000120 -10149,5,109000110 -10150,5,105000120 -10151,1,101000000 -10152,2,101000001 -10153,3,101000002 -10154,4,101000008 -10155,5,101000004 -10156,1,109000000 -10157,2,109000001 -10158,3,109000002 -10159,4,109000003 -10160,5,109000004 -10161,4,110024 -10162,4,110034 -10163,4,110044 -10164,4,110054 -10165,3,110060 -10166,3,110070 -10167,3,110080 -10168,3,110090 -10169,3,110100 -10170,3,110110 -10171,3,110120 -10172,3,110130 -10173,3,110140 -10174,3,110150 -10175,3,110160 -10176,3,110170 -10177,3,110180 -10178,3,110190 -10179,3,110200 -10180,3,110210 -10181,3,110220 -10182,3,110230 -10183,3,110240 -10184,3,110250 -10185,3,110260 -10186,3,110270 -10187,3,110620 -10188,3,110670 -10189,4,140000 -10190,4,150010 -10191,4,150020 -10192,4,150030 -10193,4,150040 -10195,3,102000060 -10196,3,102000070 -10197,3,102000080 -10198,3,103000050 -10199,3,105000050 -10200,3,107000060 -10201,3,107000070 -10202,3,107000080 -10203,3,108000050 -10204,3,109000050 -10205,3,103000060 -10206,3,103000070 -10207,3,103000080 -10208,3,110000050 -10209,4,102000100 -10210,4,102000110 -10211,4,102000350 -10212,4,106000090 -10213,4,109000090 -10214,4,107000100 -10215,4,103000090 -10216,4,102000090 -10217,4,103000100 -10218,5,102000120 -10219,5,107000110 -10220,5,103000120 -10221,1,102000000 -10222,2,102000001 -10223,3,102000002 -10224,4,102000003 -10225,5,102000004 -10226,1,105000000 -10227,2,105000001 -10228,3,105000002 -10229,4,105000003 -10230,5,105000004 -10231,1,112000000 -10232,2,112000001 -10233,3,112000002 -10234,4,112000003 -10235,5,112000004 -10236,4,120024 -10237,4,120034 -10238,4,120044 -10239,4,120054 -10240,3,120241 -10241,3,120251 -10242,3,120261 -10243,3,120271 -10244,3,120300 -10245,3,120310 -10246,3,120320 -10247,3,120330 -10248,3,120340 -10249,3,120350 -10250,3,120360 -10251,3,120370 -10252,3,120380 -10253,3,120390 -10254,3,120400 -10255,3,120410 -10256,3,120420 -10257,3,120430 -10258,3,120450 -10259,3,120460 -10260,3,120550 -10261,3,120560 -10262,3,120570 -10263,3,120990 -10264,3,121000 -10265,3,121010 -10266,3,121020 -10267,3,121100 -10268,4,140000 -10269,4,150010 -10270,4,150020 -10271,4,150030 -10272,4,150040 -10274,3,118000020 -10275,3,118000030 -10276,3,118000040 -10277,3,106000060 -10278,3,106000070 -10279,3,106000080 -10280,3,101000070 -10281,3,110000060 -10282,3,104000060 -10283,3,104000070 -10284,3,104000080 -10285,3,102000050 -10286,3,104000170 -10287,3,104000260 -10288,4,118000050 -10289,4,118000060 -10290,4,106000100 -10291,4,106000110 -10292,4,104000090 -10293,4,104000100 -10294,4,104000110 -10295,4,104000270 -10296,4,107000090 -10297,4,104000180 -10298,5,118000070 -10299,5,106000120 -10300,5,104000120 -10301,5,104000190 -10302,1,103000000 -10303,2,103000001 -10304,3,103000002 -10305,4,103000003 -10306,5,103000004 -10307,1,111000000 -10308,2,111000001 -10309,3,111000002 -10310,4,111000003 -10311,5,111000004 -10312,1,115000000 -10313,2,115000001 -10314,3,115000002 -10315,4,115000003 -10316,5,115000004 -10317,4,130024 -10318,4,130034 -10319,4,130044 -10320,4,130054 -10321,3,130060 -10322,3,130070 -10323,3,130080 -10324,3,130090 -10325,3,130100 -10326,3,130110 -10327,3,130120 -10328,3,130130 -10329,3,130140 -10330,3,130150 -10331,3,130160 -10332,3,130170 -10333,3,130180 -10334,3,130190 -10335,3,130200 -10336,3,130420 -10337,3,130510 -10338,3,130520 -10339,3,130531 -10340,3,130540 -10341,3,130660 -10342,3,130790 -10343,3,130800 -10344,3,131130 -10345,4,140000 -10346,4,150010 -10347,4,150020 -10348,4,150030 -10349,4,150040 -10351,1,101000010 -10352,1,102000010 -10353,1,103000010 -10354,1,104000010 -10355,1,105000010 -10356,1,106000010 -10357,1,107000010 -10358,1,108000010 -10359,1,109000010 -10360,1,110000010 -10361,2,101000020 -10362,2,101000030 -10363,2,102000020 -10364,2,102000030 -10365,2,102000040 -10366,2,103000020 -10367,2,103000030 -10368,2,103000040 -10369,2,104000020 -10370,2,104000030 -10371,2,104000040 -10372,2,105000020 -10373,2,105000030 -10374,2,105000040 -10375,2,106000020 -10376,2,106000030 -10377,2,106000040 -10378,2,107000020 -10379,2,107000030 -10380,2,107000040 -10381,2,108000020 -10382,2,108000030 -10383,2,108000040 -10384,2,109000020 -10385,2,109000030 -10386,2,109000040 -10387,2,110000020 -10388,2,110000030 -10389,2,110000040 -10390,2,118000010 -10391,3,101000050 -10392,3,101000060 -10393,3,101000080 -10394,3,101000040 -10395,3,109000060 -10396,3,109000070 -10397,3,109000080 -10398,3,105000060 -10399,3,105000070 -10400,3,105000080 -10401,3,104000050 -10402,3,106000050 -10403,3,102000060 -10404,3,102000070 -10405,3,102000080 -10406,3,103000050 -10407,3,105000050 -10408,3,107000060 -10409,3,107000070 -10410,3,107000080 -10411,3,108000050 -10412,3,109000050 -10413,3,103000060 -10414,3,103000070 -10415,3,103000080 -10416,3,110000050 -10417,3,106000060 -10418,3,106000070 -10419,3,106000080 -10420,3,101000070 -10421,3,110000060 -10422,3,104000060 -10423,3,104000070 -10424,3,104000080 -10425,3,102000050 -10426,3,104000170 -10427,3,104000260 -10428,3,111000010 -10429,3,111000020 -10430,3,111000030 -10431,3,112000020 -10432,3,112000030 -10433,3,108000060 -10434,3,108000070 -10435,3,108000080 -10436,3,107000050 -10437,3,112000010 -10438,3,110000070 -10439,3,110000080 -10440,3,118000020 -10441,3,118000030 -10442,3,118000040 -10443,4,101000090 -10444,4,101000100 -10445,4,101000110 -10446,4,109000100 -10447,4,105000100 -10448,4,105000110 -10449,4,108000090 -10450,4,110000090 -10451,4,102000100 -10452,4,102000110 -10453,4,106000090 -10454,4,109000090 -10455,4,107000100 -10456,4,103000090 -10457,4,102000090 -10458,4,103000100 -10459,4,106000100 -10460,4,106000110 -10461,4,104000090 -10462,4,104000100 -10463,4,104000110 -10464,4,107000090 -10465,4,104000180 -10466,4,111000040 -10467,4,112000040 -10468,4,108000100 -10469,4,105000090 -10470,4,110000100 -10471,4,118000050 -10472,4,118000060 -10473,5,101000120 -10474,5,109000110 -10475,5,105000120 -10476,5,102000120 -10477,5,107000110 -10478,5,103000120 -10479,5,106000120 -10480,5,104000120 -10481,5,104000190 -10482,5,111000060 -10483,5,112000060 -10484,5,108000110 -10485,5,110000110 -10486,5,118000070 -10487,1,201000010 -10488,1,292000010 -10489,1,299000040 -10490,1,299000070 -10491,1,299000110 -10492,1,299000120 -10493,1,299000140 -10494,2,202000010 -10495,2,290000010 -10496,2,299000010 -10497,2,299000150 -10498,2,299000190 -10499,2,299000200 -10500,2,299000210 -10501,3,298000050 -10502,3,298000060 -10503,3,299000060 -10504,3,299000170 -10505,3,290000120 -10506,3,291000050 -10507,3,292000020 -10508,4,299000670 -10509,4,299000680 -10510,4,204000010 -10511,4,209000040 -10512,4,202000070 -10513,5,297000100 -10514,5,291000020 -10515,5,297000130 -10516,5,297000140 -10517,5,203000010 -10518,5,206000030 -10519,5,203000050 -10520,1,170002 -10521,1,180002 -10522,2,170003 -10523,2,180003 -10524,3,170004 -10525,3,180004 -10526,4,140000 -10527,5,150010 -10528,5,150020 -10529,5,150030 -10530,5,150040 -10532,1,101000010 -10533,1,102000010 -10534,1,103000010 -10535,1,104000010 -10536,1,105000010 -10537,1,106000010 -10538,1,107000010 -10539,1,108000010 -10540,1,109000010 -10541,1,110000010 -10542,2,101000020 -10543,2,101000030 -10544,2,102000020 -10545,2,102000030 -10546,2,102000040 -10547,2,103000020 -10548,2,103000030 -10549,2,103000040 -10550,2,104000020 -10551,2,104000030 -10552,2,104000040 -10553,2,105000020 -10554,2,105000030 -10555,2,105000040 -10556,2,106000020 -10557,2,106000030 -10558,2,106000040 -10559,2,107000020 -10560,2,107000030 -10561,2,107000040 -10562,2,108000020 -10563,2,108000030 -10564,2,108000040 -10565,2,109000020 -10566,2,109000030 -10567,2,109000040 -10568,2,110000020 -10569,2,110000030 -10570,2,110000040 -10571,2,118000010 -10572,3,101000050 -10573,3,101000060 -10574,3,101000080 -10575,3,101000040 -10576,3,109000060 -10577,3,109000070 -10578,3,109000080 -10579,3,105000060 -10580,3,105000070 -10581,3,105000080 -10582,3,104000050 -10583,3,106000050 -10584,3,102000060 -10585,3,102000070 -10586,3,102000080 -10587,3,103000050 -10588,3,105000050 -10589,3,107000060 -10590,3,107000070 -10591,3,107000080 -10592,3,108000050 -10593,3,109000050 -10594,3,103000060 -10595,3,103000070 -10596,3,103000080 -10597,3,110000050 -10598,3,106000060 -10599,3,106000070 -10600,3,106000080 -10601,3,101000070 -10602,3,110000060 -10603,3,104000060 -10604,3,104000070 -10605,3,104000080 -10606,3,102000050 -10607,3,104000170 -10608,3,104000260 -10609,3,111000010 -10610,3,111000020 -10611,3,111000030 -10612,3,112000020 -10613,3,112000030 -10614,3,108000060 -10615,3,108000070 -10616,3,108000080 -10617,3,107000050 -10618,3,112000010 -10619,3,110000070 -10620,3,110000080 -10621,3,118000020 -10622,3,118000030 -10623,3,118000040 -10624,4,101000090 -10625,4,101000100 -10626,4,101000110 -10627,4,109000100 -10628,4,105000100 -10629,4,105000110 -10630,4,108000090 -10631,4,110000090 -10632,4,102000100 -10633,4,102000110 -10634,4,106000090 -10635,4,109000090 -10636,4,107000100 -10637,4,103000090 -10638,4,102000090 -10639,4,103000100 -10640,4,106000100 -10641,4,106000110 -10642,4,104000090 -10643,4,104000100 -10644,4,104000110 -10645,4,107000090 -10646,4,104000180 -10647,4,111000040 -10648,4,112000040 -10649,4,108000100 -10650,4,105000090 -10651,4,110000100 -10652,4,118000050 -10653,4,118000060 -10654,5,101000120 -10655,5,109000110 -10656,5,105000120 -10657,5,102000120 -10658,5,107000110 -10659,5,103000120 -10660,5,106000120 -10661,5,104000120 -10662,5,104000190 -10663,5,111000060 -10664,5,112000060 -10665,5,108000110 -10666,5,110000110 -10667,5,118000070 -10668,1,201000010 -10669,1,292000010 -10670,1,299000040 -10671,1,299000070 -10672,1,299000110 -10673,1,299000120 -10674,1,299000140 -10675,2,202000010 -10676,2,290000010 -10677,2,299000010 -10678,2,299000150 -10679,2,299000190 -10680,2,299000200 -10681,2,299000210 -10682,3,298000050 -10683,3,298000060 -10684,3,299000060 -10685,3,299000170 -10686,3,290000120 -10687,3,291000050 -10688,3,292000020 -10689,4,299000670 -10690,4,299000680 -10691,4,204000010 -10692,4,209000040 -10693,4,202000070 -10694,5,297000100 -10695,5,291000020 -10696,5,297000130 -10697,5,297000140 -10698,5,203000010 -10699,5,206000030 -10700,5,203000050 -10701,2,170003 -10702,2,180003 -10703,3,170004 -10704,3,180004 -10705,4,140000 -10706,5,150010 -10707,5,150020 -10708,5,150030 -10709,5,150040 -10711,3,101000050 -10712,3,101000060 -10713,3,101000080 -10714,3,101000040 -10715,3,109000060 -10716,3,109000070 -10717,3,109000080 -10718,3,105000060 -10719,3,105000070 -10720,3,105000080 -10721,3,104000050 -10722,3,106000050 -10723,3,102000060 -10724,3,102000070 -10725,3,102000080 -10726,3,103000050 -10727,3,105000050 -10728,3,107000060 -10729,3,107000070 -10730,3,107000080 -10731,3,108000050 -10732,3,109000050 -10733,3,103000060 -10734,3,103000070 -10735,3,103000080 -10736,3,110000050 -10737,3,106000060 -10738,3,106000070 -10739,3,106000080 -10740,3,101000070 -10741,3,110000060 -10742,3,104000060 -10743,3,104000070 -10744,3,104000080 -10745,3,102000050 -10746,3,104000170 -10747,3,104000260 -10748,3,111000010 -10749,3,111000020 -10750,3,111000030 -10751,3,112000020 -10752,3,112000030 -10753,3,108000060 -10754,3,108000070 -10755,3,108000080 -10756,3,107000050 -10757,3,112000010 -10758,3,110000070 -10759,3,110000080 -10760,3,118000020 -10761,3,118000030 -10762,3,118000040 -10763,4,101000090 -10764,4,101000100 -10765,4,101000110 -10766,4,109000100 -10767,4,105000100 -10768,4,105000110 -10769,4,108000090 -10770,4,110000090 -10771,4,102000100 -10772,4,102000110 -10773,4,106000090 -10774,4,109000090 -10775,4,107000100 -10776,4,103000090 -10777,4,102000090 -10778,4,103000100 -10779,4,106000100 -10780,4,106000110 -10781,4,104000090 -10782,4,104000100 -10783,4,104000110 -10784,4,107000090 -10785,4,104000180 -10786,4,111000040 -10787,4,112000040 -10788,4,108000100 -10789,4,105000090 -10790,4,110000100 -10791,4,118000050 -10792,4,118000060 -10793,5,101000120 -10794,5,109000110 -10795,5,105000120 -10796,5,102000120 -10797,5,107000110 -10798,5,103000120 -10799,5,106000120 -10800,5,104000120 -10801,5,104000190 -10802,5,111000060 -10803,5,112000060 -10804,5,108000110 -10805,5,110000110 -10806,5,118000070 -10807,1,201000010 -10808,1,292000010 -10809,1,299000040 -10810,1,299000070 -10811,1,299000110 -10812,1,299000120 -10813,1,299000140 -10814,2,202000010 -10815,2,290000010 -10816,2,299000010 -10817,2,299000150 -10818,2,299000190 -10819,2,299000200 -10820,2,299000210 -10821,3,298000050 -10822,3,298000060 -10823,3,299000060 -10824,3,299000170 -10825,3,290000120 -10826,3,291000050 -10827,3,292000020 -10828,4,299000670 -10829,4,299000680 -10830,4,204000010 -10831,4,209000040 -10832,4,202000070 -10833,5,297000100 -10834,5,291000020 -10835,5,297000130 -10836,5,297000140 -10837,5,203000010 -10838,5,206000030 -10839,5,203000050 -10840,3,170004 -10841,3,180004 -10842,4,140000 -10843,5,150010 -10844,5,150020 -10845,5,150030 -10846,5,150040 -10848,3,101000050 -10849,3,101000060 -10850,3,101000080 -10851,3,101000040 -10852,3,109000060 -10853,3,109000070 -10854,3,109000080 -10855,3,105000060 -10856,3,105000070 -10857,3,105000080 -10858,3,104000050 -10859,3,106000050 -10860,3,102000060 -10861,3,102000070 -10862,3,102000080 -10863,3,103000050 -10864,3,105000050 -10865,3,107000060 -10866,3,107000070 -10867,3,107000080 -10868,3,108000050 -10869,3,109000050 -10870,3,103000060 -10871,3,103000070 -10872,3,103000080 -10873,3,110000050 -10874,3,106000060 -10875,3,106000070 -10876,3,106000080 -10877,3,101000070 -10878,3,110000060 -10879,3,104000060 -10880,3,104000070 -10881,3,104000080 -10882,3,102000050 -10883,3,104000170 -10884,3,104000260 -10885,3,111000010 -10886,3,111000020 -10887,3,111000030 -10888,3,112000020 -10889,3,112000030 -10890,3,108000060 -10891,3,108000070 -10892,3,108000080 -10893,3,107000050 -10894,3,112000010 -10895,3,110000070 -10896,3,110000080 -10897,3,118000020 -10898,3,118000030 -10899,3,118000040 -10900,4,101000090 -10901,4,101000100 -10902,4,101000110 -10903,4,109000100 -10904,4,105000100 -10905,4,105000110 -10906,4,108000090 -10907,4,110000090 -10908,4,102000100 -10909,4,102000110 -10910,4,106000090 -10911,4,109000090 -10912,4,107000100 -10913,4,103000090 -10914,4,102000090 -10915,4,103000100 -10916,4,106000100 -10917,4,106000110 -10918,4,104000090 -10919,4,104000100 -10920,4,104000110 -10921,4,107000090 -10922,4,104000180 -10923,4,111000040 -10924,4,112000040 -10925,4,108000100 -10926,4,105000090 -10927,4,110000100 -10928,4,118000050 -10929,4,118000060 -10930,5,101000120 -10931,5,109000110 -10932,5,105000120 -10933,5,102000120 -10934,5,107000110 -10935,5,103000120 -10936,5,106000120 -10937,5,104000120 -10938,5,104000190 -10939,5,111000060 -10940,5,112000060 -10941,5,108000110 -10942,5,110000110 -10943,5,118000070 -10944,1,201000010 -10945,1,292000010 -10946,1,299000040 -10947,1,299000070 -10948,1,299000110 -10949,1,299000120 -10950,1,299000140 -10951,2,202000010 -10952,2,290000010 -10953,2,299000010 -10954,2,299000150 -10955,2,299000190 -10956,2,299000200 -10957,2,299000210 -10958,3,298000050 -10959,3,298000060 -10960,3,299000060 -10961,3,299000170 -10962,3,290000120 -10963,3,291000050 -10964,3,292000020 -10965,4,299000670 -10966,4,299000680 -10967,4,204000010 -10968,4,209000040 -10969,4,202000070 -10970,5,297000100 -10971,5,291000020 -10972,5,297000130 -10973,5,297000140 -10974,5,203000010 -10975,5,206000030 -10976,5,203000050 -10977,3,170004 -10978,3,180004 -10979,4,140000 -10980,5,150010 -10981,5,150020 -10982,5,150030 -10983,5,150040 -10985,3,101000050 -10986,3,101000060 -10987,3,101000080 -10988,3,101000040 -10989,3,109000060 -10990,3,109000070 -10991,3,109000080 -10992,3,105000060 -10993,3,105000070 -10994,3,105000080 -10995,3,104000050 -10996,3,106000050 -10997,3,102000060 -10998,3,102000070 -10999,3,102000080 -11000,3,103000050 -11001,3,105000050 -11002,3,107000060 -11003,3,107000070 -11004,3,107000080 -11005,3,108000050 -11006,3,109000050 -11007,3,103000060 -11008,3,103000070 -11009,3,103000080 -11010,3,110000050 -11011,3,106000060 -11012,3,106000070 -11013,3,106000080 -11014,3,101000070 -11015,3,110000060 -11016,3,104000060 -11017,3,104000070 -11018,3,104000080 -11019,3,102000050 -11020,3,104000170 -11021,3,104000260 -11022,3,111000010 -11023,3,111000020 -11024,3,111000030 -11025,3,112000020 -11026,3,112000030 -11027,3,108000060 -11028,3,108000070 -11029,3,108000080 -11030,3,107000050 -11031,3,112000010 -11032,3,110000070 -11033,3,110000080 -11034,3,118000020 -11035,3,118000030 -11036,3,118000040 -11037,4,101000090 -11038,4,101000100 -11039,4,101000110 -11040,4,109000100 -11041,4,105000100 -11042,4,105000110 -11043,4,108000090 -11044,4,110000090 -11045,4,102000100 -11046,4,102000110 -11047,4,106000090 -11048,4,109000090 -11049,4,107000100 -11050,4,103000090 -11051,4,102000090 -11052,4,103000100 -11053,4,106000100 -11054,4,106000110 -11055,4,104000090 -11056,4,104000100 -11057,4,104000110 -11058,4,107000090 -11059,4,104000180 -11060,4,111000040 -11061,4,112000040 -11062,4,108000100 -11063,4,105000090 -11064,4,110000100 -11065,4,118000050 -11066,4,118000060 -11067,5,101000120 -11068,5,109000110 -11069,5,105000120 -11070,5,102000120 -11071,5,107000110 -11072,5,103000120 -11073,5,106000120 -11074,5,104000120 -11075,5,104000190 -11076,5,111000060 -11077,5,112000060 -11078,5,108000110 -11079,5,110000110 -11080,5,118000070 -11081,1,201000010 -11082,1,292000010 -11083,1,299000040 -11084,1,299000070 -11085,1,299000110 -11086,1,299000120 -11087,1,299000140 -11088,2,202000010 -11089,2,290000010 -11090,2,299000010 -11091,2,299000150 -11092,2,299000190 -11093,2,299000200 -11094,2,299000210 -11095,3,298000050 -11096,3,298000060 -11097,3,299000060 -11098,3,299000170 -11099,3,290000120 -11100,3,291000050 -11101,3,292000020 -11102,4,299000670 -11103,4,299000680 -11104,4,204000010 -11105,4,209000040 -11106,4,202000070 -11107,5,297000100 -11108,5,291000020 -11109,5,297000130 -11110,5,297000140 -11111,5,203000010 -11112,5,206000030 -11113,5,203000050 -11114,3,170004 -11115,3,180004 -11116,4,140000 -11117,5,150010 -11118,5,150020 -11119,5,150030 -11120,5,150040 -11121,5,190000 -11122,5,200000 -11123,5,210000 -11125,3,118000020 -11126,3,118000030 -11127,3,118000040 -11128,3,106000060 -11129,3,106000070 -11130,3,106000080 -11131,3,101000070 -11132,3,110000060 -11133,3,104000060 -11134,3,104000070 -11135,3,104000080 -11136,3,102000050 -11137,3,104000170 -11138,3,104000260 -11139,4,118000050 -11140,4,118000060 -11141,4,106000100 -11142,4,106000110 -11143,4,104000090 -11144,4,104000100 -11145,4,104000110 -11146,4,104000270 -11147,4,107000090 -11148,4,104000180 -11149,5,118000070 -11150,5,106000120 -11151,5,104000120 -11152,5,104000190 -11153,1,103000000 -11154,2,103000001 -11155,3,103000002 -11156,4,103000003 -11157,5,103000004 -11158,1,111000000 -11159,2,111000001 -11160,3,111000002 -11161,4,111000003 -11162,5,111000004 -11163,1,115000000 -11164,2,115000001 -11165,3,115000002 -11166,4,115000003 -11167,5,115000004 -11168,3,170004 -11169,4,170005 -11170,3,180004 -11171,4,180005 -11172,4,140000 -11173,4,150010 -11174,4,150020 -11175,4,150030 -11176,4,150040 -11178,3,111000010 -11179,3,111000020 -11180,3,111000030 -11181,3,112000020 -11182,3,112000030 -11183,3,108000060 -11184,3,108000070 -11185,3,108000080 -11186,3,107000050 -11187,3,112000010 -11188,3,110000070 -11189,3,110000080 -11190,4,111000040 -11191,4,112000040 -11192,4,108000100 -11193,4,105000090 -11194,4,110000100 -11195,5,111000060 -11196,5,112000060 -11197,5,108000110 -11198,5,110000110 -11199,1,108000000 -11200,2,108000001 -11201,3,108000002 -11202,4,108000003 -11203,5,108000004 -11204,1,107000000 -11205,2,107000001 -11206,3,107000002 -11207,4,107000003 -11208,5,107000004 -11209,1,120000000 -11210,2,120000001 -11211,3,120000002 -11212,4,120000003 -11213,5,120000004 -11214,4,110024 -11215,4,110034 -11216,4,110044 -11217,4,110054 -11218,3,110060 -11219,3,110070 -11220,3,110080 -11221,3,110090 -11222,3,110100 -11223,3,110110 -11224,3,110120 -11225,3,110130 -11226,3,110140 -11227,3,110150 -11228,3,110160 -11229,3,110170 -11230,3,110180 -11231,3,110190 -11232,3,110200 -11233,3,110210 -11234,3,110220 -11235,3,110230 -11236,3,110240 -11237,3,110250 -11238,3,110260 -11239,3,110270 -11240,3,110620 -11241,3,110670 -11242,4,140000 -11243,4,150010 -11244,4,150020 -11245,4,150030 -11246,4,150040 -11248,3,101000050 -11249,3,101000060 -11250,3,101000080 -11251,3,101000040 -11252,3,109000060 -11253,3,109000070 -11254,3,109000080 -11255,3,105000060 -11256,3,105000070 -11257,3,105000080 -11258,3,104000050 -11259,3,106000050 -11260,4,101000090 -11261,4,101000100 -11262,4,101000110 -11263,4,109000100 -11264,4,105000100 -11265,4,105000110 -11266,4,108000090 -11267,4,110000090 -11268,5,101000120 -11269,5,109000110 -11270,5,105000120 -11271,1,101000000 -11272,2,101000001 -11273,3,101000002 -11274,4,101000008 -11275,5,101000004 -11276,1,109000000 -11277,2,109000001 -11278,3,109000002 -11279,4,109000003 -11280,5,109000004 -11281,4,120024 -11282,4,120034 -11283,4,120044 -11284,4,120054 -11285,3,120241 -11286,3,120251 -11287,3,120261 -11288,3,120271 -11289,3,120300 -11290,3,120310 -11291,3,120320 -11292,3,120330 -11293,3,120340 -11294,3,120350 -11295,3,120360 -11296,3,120370 -11297,3,120380 -11298,3,120390 -11299,3,120400 -11300,3,120410 -11301,3,120420 -11302,3,120430 -11303,3,120450 -11304,3,120460 -11305,3,120550 -11306,3,120560 -11307,3,120570 -11308,3,120990 -11309,3,121000 -11310,3,121010 -11311,3,121020 -11312,3,121100 -11313,4,140000 -11314,4,150010 -11315,4,150020 -11316,4,150030 -11317,4,150040 -11319,3,102000060 -11320,3,102000070 -11321,3,102000080 -11322,3,103000050 -11323,3,105000050 -11324,3,107000060 -11325,3,107000070 -11326,3,107000080 -11327,3,108000050 -11328,3,109000050 -11329,3,103000060 -11330,3,103000070 -11331,3,103000080 -11332,3,110000050 -11333,4,102000100 -11334,4,102000110 -11335,4,102000350 -11336,4,106000090 -11337,4,109000090 -11338,4,107000100 -11339,4,103000090 -11340,4,102000090 -11341,4,103000100 -11342,5,102000120 -11343,5,107000110 -11344,5,103000120 -11345,1,102000000 -11346,2,102000001 -11347,3,102000002 -11348,4,102000003 -11349,5,102000004 -11350,1,105000000 -11351,2,105000001 -11352,3,105000002 -11353,4,105000003 -11354,5,105000004 -11355,1,112000000 -11356,2,112000001 -11357,3,112000002 -11358,4,112000003 -11359,5,112000004 -11360,4,130024 -11361,4,130034 -11362,4,130044 -11363,4,130054 -11364,3,130060 -11365,3,130070 -11366,3,130080 -11367,3,130090 -11368,3,130100 -11369,3,130110 -11370,3,130120 -11371,3,130130 -11372,3,130140 -11373,3,130150 -11374,3,130160 -11375,3,130170 -11376,3,130180 -11377,3,130190 -11378,3,130200 -11379,3,130420 -11380,3,130510 -11381,3,130520 -11382,3,130531 -11383,3,130540 -11384,3,130660 -11385,3,130790 -11386,3,130800 -11387,3,131130 -11388,4,140000 -11389,4,150010 -11390,4,150020 -11391,4,150030 -11392,4,150040 -11394,1,101000010 -11395,1,102000010 -11396,1,103000010 -11397,1,104000010 -11398,1,105000010 -11399,1,106000010 -11400,1,107000010 -11401,1,108000010 -11402,1,109000010 -11403,1,110000010 -11404,2,101000020 -11405,2,101000030 -11406,2,102000020 -11407,2,102000030 -11408,2,102000040 -11409,2,103000020 -11410,2,103000030 -11411,2,103000040 -11412,2,104000020 -11413,2,104000030 -11414,2,104000040 -11415,2,105000020 -11416,2,105000030 -11417,2,105000040 -11418,2,106000020 -11419,2,106000030 -11420,2,106000040 -11421,2,107000020 -11422,2,107000030 -11423,2,107000040 -11424,2,108000020 -11425,2,108000030 -11426,2,108000040 -11427,2,109000020 -11428,2,109000030 -11429,2,109000040 -11430,2,110000020 -11431,2,110000030 -11432,2,110000040 -11433,2,118000010 -11434,3,101000050 -11435,3,101000060 -11436,3,101000080 -11437,3,101000040 -11438,3,109000060 -11439,3,109000070 -11440,3,109000080 -11441,3,105000060 -11442,3,105000070 -11443,3,105000080 -11444,3,104000050 -11445,3,106000050 -11446,3,102000060 -11447,3,102000070 -11448,3,102000080 -11449,3,103000050 -11450,3,105000050 -11451,3,107000060 -11452,3,107000070 -11453,3,107000080 -11454,3,108000050 -11455,3,109000050 -11456,3,103000060 -11457,3,103000070 -11458,3,103000080 -11459,3,110000050 -11460,3,106000060 -11461,3,106000070 -11462,3,106000080 -11463,3,101000070 -11464,3,110000060 -11465,3,104000060 -11466,3,104000070 -11467,3,104000080 -11468,3,102000050 -11469,3,104000170 -11470,3,104000260 -11471,3,111000010 -11472,3,111000020 -11473,3,111000030 -11474,3,112000020 -11475,3,112000030 -11476,3,108000060 -11477,3,108000070 -11478,3,108000080 -11479,3,107000050 -11480,3,112000010 -11481,3,110000070 -11482,3,110000080 -11483,3,118000020 -11484,3,118000030 -11485,3,118000040 -11486,4,101000090 -11487,4,101000100 -11488,4,101000110 -11489,4,109000100 -11490,4,105000100 -11491,4,105000110 -11492,4,108000090 -11493,4,110000090 -11494,4,102000100 -11495,4,102000110 -11496,4,106000090 -11497,4,109000090 -11498,4,107000100 -11499,4,103000090 -11500,4,102000090 -11501,4,103000100 -11502,4,106000100 -11503,4,106000110 -11504,4,104000090 -11505,4,104000100 -11506,4,104000110 -11507,4,107000090 -11508,4,104000180 -11509,4,111000040 -11510,4,112000040 -11511,4,108000100 -11512,4,105000090 -11513,4,110000100 -11514,4,118000050 -11515,4,118000060 -11516,5,101000120 -11517,5,109000110 -11518,5,105000120 -11519,5,102000120 -11520,5,107000110 -11521,5,103000120 -11522,5,106000120 -11523,5,104000120 -11524,5,104000190 -11525,5,111000060 -11526,5,112000060 -11527,5,108000110 -11528,5,110000110 -11529,5,118000070 -11530,1,201000010 -11531,1,292000010 -11532,1,299000040 -11533,1,299000070 -11534,1,299000110 -11535,1,299000120 -11536,1,299000140 -11537,2,202000010 -11538,2,290000010 -11539,2,299000010 -11540,2,299000150 -11541,2,299000190 -11542,2,299000200 -11543,2,299000210 -11544,3,298000050 -11545,3,298000060 -11546,3,299000060 -11547,3,299000170 -11548,3,290000120 -11549,3,291000050 -11550,3,292000020 -11551,4,299000670 -11552,4,299000680 -11553,4,204000010 -11554,4,209000040 -11555,4,202000070 -11556,4,209000070 -11557,5,297000100 -11558,5,291000020 -11559,5,297000130 -11560,5,297000140 -11561,5,203000010 -11562,5,206000030 -11563,5,203000050 -11564,5,202000090 -11565,1,170002 -11566,1,180002 -11567,2,170003 -11568,2,180003 -11569,3,170004 -11570,3,180004 -11571,4,140000 -11572,5,150010 -11573,5,150020 -11574,5,150030 -11575,5,150040 -11577,1,101000010 -11578,1,102000010 -11579,1,103000010 -11580,1,104000010 -11581,1,105000010 -11582,1,106000010 -11583,1,107000010 -11584,1,108000010 -11585,1,109000010 -11586,1,110000010 -11587,2,101000020 -11588,2,101000030 -11589,2,102000020 -11590,2,102000030 -11591,2,102000040 -11592,2,103000020 -11593,2,103000030 -11594,2,103000040 -11595,2,104000020 -11596,2,104000030 -11597,2,104000040 -11598,2,105000020 -11599,2,105000030 -11600,2,105000040 -11601,2,106000020 -11602,2,106000030 -11603,2,106000040 -11604,2,107000020 -11605,2,107000030 -11606,2,107000040 -11607,2,108000020 -11608,2,108000030 -11609,2,108000040 -11610,2,109000020 -11611,2,109000030 -11612,2,109000040 -11613,2,110000020 -11614,2,110000030 -11615,2,110000040 -11616,2,118000010 -11617,3,101000050 -11618,3,101000060 -11619,3,101000080 -11620,3,101000040 -11621,3,109000060 -11622,3,109000070 -11623,3,109000080 -11624,3,105000060 -11625,3,105000070 -11626,3,105000080 -11627,3,104000050 -11628,3,106000050 -11629,3,102000060 -11630,3,102000070 -11631,3,102000080 -11632,3,103000050 -11633,3,105000050 -11634,3,107000060 -11635,3,107000070 -11636,3,107000080 -11637,3,108000050 -11638,3,109000050 -11639,3,103000060 -11640,3,103000070 -11641,3,103000080 -11642,3,110000050 -11643,3,106000060 -11644,3,106000070 -11645,3,106000080 -11646,3,101000070 -11647,3,110000060 -11648,3,104000060 -11649,3,104000070 -11650,3,104000080 -11651,3,102000050 -11652,3,104000170 -11653,3,104000260 -11654,3,111000010 -11655,3,111000020 -11656,3,111000030 -11657,3,112000020 -11658,3,112000030 -11659,3,108000060 -11660,3,108000070 -11661,3,108000080 -11662,3,107000050 -11663,3,112000010 -11664,3,110000070 -11665,3,110000080 -11666,3,118000020 -11667,3,118000030 -11668,3,118000040 -11669,4,101000090 -11670,4,101000100 -11671,4,101000110 -11672,4,109000100 -11673,4,105000100 -11674,4,105000110 -11675,4,108000090 -11676,4,110000090 -11677,4,102000100 -11678,4,102000110 -11679,4,106000090 -11680,4,109000090 -11681,4,107000100 -11682,4,103000090 -11683,4,102000090 -11684,4,103000100 -11685,4,106000100 -11686,4,106000110 -11687,4,104000090 -11688,4,104000100 -11689,4,104000110 -11690,4,107000090 -11691,4,104000180 -11692,4,111000040 -11693,4,112000040 -11694,4,108000100 -11695,4,105000090 -11696,4,110000100 -11697,4,118000050 -11698,4,118000060 -11699,5,101000120 -11700,5,109000110 -11701,5,105000120 -11702,5,102000120 -11703,5,107000110 -11704,5,103000120 -11705,5,106000120 -11706,5,104000120 -11707,5,104000190 -11708,5,111000060 -11709,5,112000060 -11710,5,108000110 -11711,5,110000110 -11712,5,118000070 -11713,1,201000010 -11714,1,292000010 -11715,1,299000040 -11716,1,299000070 -11717,1,299000110 -11718,1,299000120 -11719,1,299000140 -11720,2,202000010 -11721,2,290000010 -11722,2,299000010 -11723,2,299000150 -11724,2,299000190 -11725,2,299000200 -11726,2,299000210 -11727,3,298000050 -11728,3,298000060 -11729,3,299000060 -11730,3,299000170 -11731,3,290000120 -11732,3,291000050 -11733,3,292000020 -11734,4,299000670 -11735,4,299000680 -11736,4,204000010 -11737,4,209000040 -11738,4,202000070 -11739,4,209000070 -11740,5,297000100 -11741,5,291000020 -11742,5,297000130 -11743,5,297000140 -11744,5,203000010 -11745,5,206000030 -11746,5,203000050 -11747,5,202000090 -11748,2,170003 -11749,2,180003 -11750,3,170004 -11751,3,180004 -11752,4,140000 -11753,5,150010 -11754,5,150020 -11755,5,150030 -11756,5,150040 -11758,3,101000050 -11759,3,101000060 -11760,3,101000080 -11761,3,101000040 -11762,3,109000060 -11763,3,109000070 -11764,3,109000080 -11765,3,105000060 -11766,3,105000070 -11767,3,105000080 -11768,3,104000050 -11769,3,106000050 -11770,3,102000060 -11771,3,102000070 -11772,3,102000080 -11773,3,103000050 -11774,3,105000050 -11775,3,107000060 -11776,3,107000070 -11777,3,107000080 -11778,3,108000050 -11779,3,109000050 -11780,3,103000060 -11781,3,103000070 -11782,3,103000080 -11783,3,110000050 -11784,3,106000060 -11785,3,106000070 -11786,3,106000080 -11787,3,101000070 -11788,3,110000060 -11789,3,104000060 -11790,3,104000070 -11791,3,104000080 -11792,3,102000050 -11793,3,104000170 -11794,3,104000260 -11795,3,111000010 -11796,3,111000020 -11797,3,111000030 -11798,3,112000020 -11799,3,112000030 -11800,3,108000060 -11801,3,108000070 -11802,3,108000080 -11803,3,107000050 -11804,3,112000010 -11805,3,110000070 -11806,3,110000080 -11807,3,118000020 -11808,3,118000030 -11809,3,118000040 -11810,4,101000090 -11811,4,101000100 -11812,4,101000110 -11813,4,109000100 -11814,4,105000100 -11815,4,105000110 -11816,4,108000090 -11817,4,110000090 -11818,4,102000100 -11819,4,102000110 -11820,4,106000090 -11821,4,109000090 -11822,4,107000100 -11823,4,103000090 -11824,4,102000090 -11825,4,103000100 -11826,4,106000100 -11827,4,106000110 -11828,4,104000090 -11829,4,104000100 -11830,4,104000110 -11831,4,107000090 -11832,4,104000180 -11833,4,111000040 -11834,4,112000040 -11835,4,108000100 -11836,4,105000090 -11837,4,110000100 -11838,4,118000050 -11839,4,118000060 -11840,5,101000120 -11841,5,109000110 -11842,5,105000120 -11843,5,102000120 -11844,5,107000110 -11845,5,103000120 -11846,5,106000120 -11847,5,104000120 -11848,5,104000190 -11849,5,111000060 -11850,5,112000060 -11851,5,108000110 -11852,5,110000110 -11853,5,118000070 -11854,1,201000010 -11855,1,292000010 -11856,1,299000040 -11857,1,299000070 -11858,1,299000110 -11859,1,299000120 -11860,1,299000140 -11861,2,202000010 -11862,2,290000010 -11863,2,299000010 -11864,2,299000150 -11865,2,299000190 -11866,2,299000200 -11867,2,299000210 -11868,3,298000050 -11869,3,298000060 -11870,3,299000060 -11871,3,299000170 -11872,3,290000120 -11873,3,291000050 -11874,3,292000020 -11875,4,299000670 -11876,4,299000680 -11877,4,204000010 -11878,4,209000040 -11879,4,202000070 -11880,4,209000070 -11881,5,297000100 -11882,5,291000020 -11883,5,297000130 -11884,5,297000140 -11885,5,203000010 -11886,5,206000030 -11887,5,203000050 -11888,5,202000090 -11889,3,170004 -11890,3,180004 -11891,4,140000 -11892,5,150010 -11893,5,150020 -11894,5,150030 -11895,5,150040 -11897,3,101000050 -11898,3,101000060 -11899,3,101000080 -11900,3,101000040 -11901,3,109000060 -11902,3,109000070 -11903,3,109000080 -11904,3,105000060 -11905,3,105000070 -11906,3,105000080 -11907,3,104000050 -11908,3,106000050 -11909,3,102000060 -11910,3,102000070 -11911,3,102000080 -11912,3,103000050 -11913,3,105000050 -11914,3,107000060 -11915,3,107000070 -11916,3,107000080 -11917,3,108000050 -11918,3,109000050 -11919,3,103000060 -11920,3,103000070 -11921,3,103000080 -11922,3,110000050 -11923,3,106000060 -11924,3,106000070 -11925,3,106000080 -11926,3,101000070 -11927,3,110000060 -11928,3,104000060 -11929,3,104000070 -11930,3,104000080 -11931,3,102000050 -11932,3,104000170 -11933,3,104000260 -11934,3,111000010 -11935,3,111000020 -11936,3,111000030 -11937,3,112000020 -11938,3,112000030 -11939,3,108000060 -11940,3,108000070 -11941,3,108000080 -11942,3,107000050 -11943,3,112000010 -11944,3,110000070 -11945,3,110000080 -11946,3,118000020 -11947,3,118000030 -11948,3,118000040 -11949,4,101000090 -11950,4,101000100 -11951,4,101000110 -11952,4,109000100 -11953,4,105000100 -11954,4,105000110 -11955,4,108000090 -11956,4,110000090 -11957,4,102000100 -11958,4,102000110 -11959,4,106000090 -11960,4,109000090 -11961,4,107000100 -11962,4,103000090 -11963,4,102000090 -11964,4,103000100 -11965,4,106000100 -11966,4,106000110 -11967,4,104000090 -11968,4,104000100 -11969,4,104000110 -11970,4,107000090 -11971,4,104000180 -11972,4,111000040 -11973,4,112000040 -11974,4,108000100 -11975,4,105000090 -11976,4,110000100 -11977,4,118000050 -11978,4,118000060 -11979,5,101000120 -11980,5,109000110 -11981,5,105000120 -11982,5,102000120 -11983,5,107000110 -11984,5,103000120 -11985,5,106000120 -11986,5,104000120 -11987,5,104000190 -11988,5,111000060 -11989,5,112000060 -11990,5,108000110 -11991,5,110000110 -11992,5,118000070 -11993,1,201000010 -11994,1,292000010 -11995,1,299000040 -11996,1,299000070 -11997,1,299000110 -11998,1,299000120 -11999,1,299000140 -12000,2,202000010 -12001,2,290000010 -12002,2,299000010 -12003,2,299000150 -12004,2,299000190 -12005,2,299000200 -12006,2,299000210 -12007,3,298000050 -12008,3,298000060 -12009,3,299000060 -12010,3,299000170 -12011,3,290000120 -12012,3,291000050 -12013,3,292000020 -12014,4,299000670 -12015,4,299000680 -12016,4,204000010 -12017,4,209000040 -12018,4,202000070 -12019,4,209000070 -12020,5,297000100 -12021,5,291000020 -12022,5,297000130 -12023,5,297000140 -12024,5,203000010 -12025,5,206000030 -12026,5,203000050 -12027,5,202000090 -12028,3,170004 -12029,3,180004 -12030,4,140000 -12031,5,150010 -12032,5,150020 -12033,5,150030 -12034,5,150040 -12036,3,101000050 -12037,3,101000060 -12038,3,101000080 -12039,3,101000040 -12040,3,109000060 -12041,3,109000070 -12042,3,109000080 -12043,3,105000060 -12044,3,105000070 -12045,3,105000080 -12046,3,104000050 -12047,3,106000050 -12048,3,102000060 -12049,3,102000070 -12050,3,102000080 -12051,3,103000050 -12052,3,105000050 -12053,3,107000060 -12054,3,107000070 -12055,3,107000080 -12056,3,108000050 -12057,3,109000050 -12058,3,103000060 -12059,3,103000070 -12060,3,103000080 -12061,3,110000050 -12062,3,106000060 -12063,3,106000070 -12064,3,106000080 -12065,3,101000070 -12066,3,110000060 -12067,3,104000060 -12068,3,104000070 -12069,3,104000080 -12070,3,102000050 -12071,3,104000170 -12072,3,104000260 -12073,3,111000010 -12074,3,111000020 -12075,3,111000030 -12076,3,112000020 -12077,3,112000030 -12078,3,108000060 -12079,3,108000070 -12080,3,108000080 -12081,3,107000050 -12082,3,112000010 -12083,3,110000070 -12084,3,110000080 -12085,3,118000020 -12086,3,118000030 -12087,3,118000040 -12088,4,101000090 -12089,4,101000100 -12090,4,101000110 -12091,4,109000100 -12092,4,105000100 -12093,4,105000110 -12094,4,108000090 -12095,4,110000090 -12096,4,102000100 -12097,4,102000110 -12098,4,106000090 -12099,4,109000090 -12100,4,107000100 -12101,4,103000090 -12102,4,102000090 -12103,4,103000100 -12104,4,106000100 -12105,4,106000110 -12106,4,104000090 -12107,4,104000100 -12108,4,104000110 -12109,4,107000090 -12110,4,104000180 -12111,4,111000040 -12112,4,112000040 -12113,4,108000100 -12114,4,105000090 -12115,4,110000100 -12116,4,118000050 -12117,4,118000060 -12118,5,101000120 -12119,5,109000110 -12120,5,105000120 -12121,5,102000120 -12122,5,107000110 -12123,5,103000120 -12124,5,106000120 -12125,5,104000120 -12126,5,104000190 -12127,5,111000060 -12128,5,112000060 -12129,5,108000110 -12130,5,110000110 -12131,5,118000070 -12132,1,201000010 -12133,1,292000010 -12134,1,299000040 -12135,1,299000070 -12136,1,299000110 -12137,1,299000120 -12138,1,299000140 -12139,2,202000010 -12140,2,290000010 -12141,2,299000010 -12142,2,299000150 -12143,2,299000190 -12144,2,299000200 -12145,2,299000210 -12146,3,298000050 -12147,3,298000060 -12148,3,299000060 -12149,3,299000170 -12150,3,290000120 -12151,3,291000050 -12152,3,292000020 -12153,4,299000670 -12154,4,299000680 -12155,4,204000010 -12156,4,209000040 -12157,4,202000070 -12158,4,209000070 -12159,5,297000100 -12160,5,291000020 -12161,5,297000130 -12162,5,297000140 -12163,5,203000010 -12164,5,206000030 -12165,5,203000050 -12166,5,202000090 -12167,3,170004 -12168,3,180004 -12169,4,140000 -12170,5,150010 -12171,5,150020 -12172,5,150030 -12173,5,150040 -12174,5,190000 -12175,5,200000 -12176,5,210000 -12178,1,101000010 -12179,1,102000010 -12180,1,103000010 -12181,1,104000010 -12182,1,105000010 -12183,1,106000010 -12184,1,107000010 -12185,1,108000010 -12186,1,109000010 -12187,1,110000010 -12188,2,101000020 -12189,2,101000030 -12190,2,102000020 -12191,2,102000030 -12192,2,102000040 -12193,2,103000020 -12194,2,103000030 -12195,2,103000040 -12196,2,104000020 -12197,2,104000030 -12198,2,104000040 -12199,2,105000020 -12200,2,105000030 -12201,2,105000040 -12202,2,106000020 -12203,2,106000030 -12204,2,106000040 -12205,2,107000020 -12206,2,107000030 -12207,2,107000040 -12208,2,108000020 -12209,2,108000030 -12210,2,108000040 -12211,2,109000020 -12212,2,109000030 -12213,2,109000040 -12214,2,110000020 -12215,2,110000030 -12216,2,110000040 -12217,2,118000010 -12218,3,101000050 -12219,3,101000060 -12220,3,101000080 -12221,3,101000040 -12222,3,109000060 -12223,3,109000070 -12224,3,109000080 -12225,3,105000060 -12226,3,105000070 -12227,3,105000080 -12228,3,104000050 -12229,3,106000050 -12230,3,102000060 -12231,3,102000070 -12232,3,102000080 -12233,3,103000050 -12234,3,105000050 -12235,3,107000060 -12236,3,107000070 -12237,3,107000080 -12238,3,108000050 -12239,3,109000050 -12240,3,103000060 -12241,3,103000070 -12242,3,103000080 -12243,3,110000050 -12244,3,106000060 -12245,3,106000070 -12246,3,106000080 -12247,3,101000070 -12248,3,110000060 -12249,3,104000060 -12250,3,104000070 -12251,3,104000080 -12252,3,102000050 -12253,3,104000170 -12254,3,104000260 -12255,3,111000010 -12256,3,111000020 -12257,3,111000030 -12258,3,112000020 -12259,3,112000030 -12260,3,108000060 -12261,3,108000070 -12262,3,108000080 -12263,3,107000050 -12264,3,112000010 -12265,3,110000070 -12266,3,110000080 -12267,3,118000020 -12268,3,118000030 -12269,3,118000040 -12270,4,101000090 -12271,4,101000100 -12272,4,101000110 -12273,4,109000100 -12274,4,105000100 -12275,4,105000110 -12276,4,108000090 -12277,4,110000090 -12278,4,102000100 -12279,4,102000110 -12280,4,106000090 -12281,4,109000090 -12282,4,107000100 -12283,4,103000090 -12284,4,102000090 -12285,4,103000100 -12286,4,106000100 -12287,4,106000110 -12288,4,104000090 -12289,4,104000100 -12290,4,104000110 -12291,4,107000090 -12292,4,104000180 -12293,4,111000040 -12294,4,112000040 -12295,4,108000100 -12296,4,105000090 -12297,4,110000100 -12298,4,118000050 -12299,4,118000060 -12300,5,101000120 -12301,5,109000110 -12302,5,105000120 -12303,5,102000120 -12304,5,107000110 -12305,5,103000120 -12306,5,106000120 -12307,5,104000120 -12308,5,104000190 -12309,5,111000060 -12310,5,112000060 -12311,5,108000110 -12312,5,110000110 -12313,5,118000070 -12314,1,201000010 -12315,1,292000010 -12316,1,299000040 -12317,1,299000070 -12318,1,299000110 -12319,1,299000120 -12320,1,299000140 -12321,2,202000010 -12322,2,290000010 -12323,2,299000010 -12324,2,299000150 -12325,2,299000190 -12326,2,299000200 -12327,2,299000210 -12328,3,298000050 -12329,3,298000060 -12330,3,299000060 -12331,3,299000170 -12332,3,290000120 -12333,3,291000050 -12334,3,292000020 -12335,4,299000670 -12336,4,299000680 -12337,4,204000010 -12338,4,209000040 -12339,4,202000070 -12340,4,209000070 -12341,4,203000110 -12342,5,297000100 -12343,5,291000020 -12344,5,297000130 -12345,5,297000140 -12346,5,203000010 -12347,5,206000030 -12348,5,203000050 -12349,5,202000090 -12350,5,204000080 -12351,1,170002 -12352,1,180002 -12353,2,170003 -12354,2,180003 -12355,3,170004 -12356,3,180004 -12357,4,140000 -12358,5,150010 -12359,5,150020 -12360,5,150030 -12361,5,150040 -12363,1,101000010 -12364,1,102000010 -12365,1,103000010 -12366,1,104000010 -12367,1,105000010 -12368,1,106000010 -12369,1,107000010 -12370,1,108000010 -12371,1,109000010 -12372,1,110000010 -12373,2,101000020 -12374,2,101000030 -12375,2,102000020 -12376,2,102000030 -12377,2,102000040 -12378,2,103000020 -12379,2,103000030 -12380,2,103000040 -12381,2,104000020 -12382,2,104000030 -12383,2,104000040 -12384,2,105000020 -12385,2,105000030 -12386,2,105000040 -12387,2,106000020 -12388,2,106000030 -12389,2,106000040 -12390,2,107000020 -12391,2,107000030 -12392,2,107000040 -12393,2,108000020 -12394,2,108000030 -12395,2,108000040 -12396,2,109000020 -12397,2,109000030 -12398,2,109000040 -12399,2,110000020 -12400,2,110000030 -12401,2,110000040 -12402,2,118000010 -12403,3,101000050 -12404,3,101000060 -12405,3,101000080 -12406,3,101000040 -12407,3,109000060 -12408,3,109000070 -12409,3,109000080 -12410,3,105000060 -12411,3,105000070 -12412,3,105000080 -12413,3,104000050 -12414,3,106000050 -12415,3,102000060 -12416,3,102000070 -12417,3,102000080 -12418,3,103000050 -12419,3,105000050 -12420,3,107000060 -12421,3,107000070 -12422,3,107000080 -12423,3,108000050 -12424,3,109000050 -12425,3,103000060 -12426,3,103000070 -12427,3,103000080 -12428,3,110000050 -12429,3,106000060 -12430,3,106000070 -12431,3,106000080 -12432,3,101000070 -12433,3,110000060 -12434,3,104000060 -12435,3,104000070 -12436,3,104000080 -12437,3,102000050 -12438,3,104000170 -12439,3,104000260 -12440,3,111000010 -12441,3,111000020 -12442,3,111000030 -12443,3,112000020 -12444,3,112000030 -12445,3,108000060 -12446,3,108000070 -12447,3,108000080 -12448,3,107000050 -12449,3,112000010 -12450,3,110000070 -12451,3,110000080 -12452,3,118000020 -12453,3,118000030 -12454,3,118000040 -12455,4,101000090 -12456,4,101000100 -12457,4,101000110 -12458,4,109000100 -12459,4,105000100 -12460,4,105000110 -12461,4,108000090 -12462,4,110000090 -12463,4,102000100 -12464,4,102000110 -12465,4,106000090 -12466,4,109000090 -12467,4,107000100 -12468,4,103000090 -12469,4,102000090 -12470,4,103000100 -12471,4,106000100 -12472,4,106000110 -12473,4,104000090 -12474,4,104000100 -12475,4,104000110 -12476,4,107000090 -12477,4,104000180 -12478,4,111000040 -12479,4,112000040 -12480,4,108000100 -12481,4,105000090 -12482,4,110000100 -12483,4,118000050 -12484,4,118000060 -12485,5,101000120 -12486,5,109000110 -12487,5,105000120 -12488,5,102000120 -12489,5,107000110 -12490,5,103000120 -12491,5,106000120 -12492,5,104000120 -12493,5,104000190 -12494,5,111000060 -12495,5,112000060 -12496,5,108000110 -12497,5,110000110 -12498,5,118000070 -12499,1,201000010 -12500,1,292000010 -12501,1,299000040 -12502,1,299000070 -12503,1,299000110 -12504,1,299000120 -12505,1,299000140 -12506,2,202000010 -12507,2,290000010 -12508,2,299000010 -12509,2,299000150 -12510,2,299000190 -12511,2,299000200 -12512,2,299000210 -12513,3,298000050 -12514,3,298000060 -12515,3,299000060 -12516,3,299000170 -12517,3,290000120 -12518,3,291000050 -12519,3,292000020 -12520,4,299000670 -12521,4,299000680 -12522,4,204000010 -12523,4,209000040 -12524,4,202000070 -12525,4,209000070 -12526,4,203000110 -12527,5,297000100 -12528,5,291000020 -12529,5,297000130 -12530,5,297000140 -12531,5,203000010 -12532,5,206000030 -12533,5,203000050 -12534,5,202000090 -12535,5,204000080 -12536,2,170003 -12537,2,180003 -12538,3,170004 -12539,3,180004 -12540,4,140000 -12541,5,150010 -12542,5,150020 -12543,5,150030 -12544,5,150040 -12546,3,101000050 -12547,3,101000060 -12548,3,101000080 -12549,3,101000040 -12550,3,109000060 -12551,3,109000070 -12552,3,109000080 -12553,3,105000060 -12554,3,105000070 -12555,3,105000080 -12556,3,104000050 -12557,3,106000050 -12558,3,102000060 -12559,3,102000070 -12560,3,102000080 -12561,3,103000050 -12562,3,105000050 -12563,3,107000060 -12564,3,107000070 -12565,3,107000080 -12566,3,108000050 -12567,3,109000050 -12568,3,103000060 -12569,3,103000070 -12570,3,103000080 -12571,3,110000050 -12572,3,106000060 -12573,3,106000070 -12574,3,106000080 -12575,3,101000070 -12576,3,110000060 -12577,3,104000060 -12578,3,104000070 -12579,3,104000080 -12580,3,102000050 -12581,3,104000170 -12582,3,104000260 -12583,3,111000010 -12584,3,111000020 -12585,3,111000030 -12586,3,112000020 -12587,3,112000030 -12588,3,108000060 -12589,3,108000070 -12590,3,108000080 -12591,3,107000050 -12592,3,112000010 -12593,3,110000070 -12594,3,110000080 -12595,3,118000020 -12596,3,118000030 -12597,3,118000040 -12598,4,101000090 -12599,4,101000100 -12600,4,101000110 -12601,4,109000100 -12602,4,105000100 -12603,4,105000110 -12604,4,108000090 -12605,4,110000090 -12606,4,102000100 -12607,4,102000110 -12608,4,106000090 -12609,4,109000090 -12610,4,107000100 -12611,4,103000090 -12612,4,102000090 -12613,4,103000100 -12614,4,106000100 -12615,4,106000110 -12616,4,104000090 -12617,4,104000100 -12618,4,104000110 -12619,4,107000090 -12620,4,104000180 -12621,4,111000040 -12622,4,112000040 -12623,4,108000100 -12624,4,105000090 -12625,4,110000100 -12626,4,118000050 -12627,4,118000060 -12628,5,101000120 -12629,5,109000110 -12630,5,105000120 -12631,5,102000120 -12632,5,107000110 -12633,5,103000120 -12634,5,106000120 -12635,5,104000120 -12636,5,104000190 -12637,5,111000060 -12638,5,112000060 -12639,5,108000110 -12640,5,110000110 -12641,5,118000070 -12642,1,201000010 -12643,1,292000010 -12644,1,299000040 -12645,1,299000070 -12646,1,299000110 -12647,1,299000120 -12648,1,299000140 -12649,2,202000010 -12650,2,290000010 -12651,2,299000010 -12652,2,299000150 -12653,2,299000190 -12654,2,299000200 -12655,2,299000210 -12656,3,298000050 -12657,3,298000060 -12658,3,299000060 -12659,3,299000170 -12660,3,290000120 -12661,3,291000050 -12662,3,292000020 -12663,4,299000670 -12664,4,299000680 -12665,4,204000010 -12666,4,209000040 -12667,4,202000070 -12668,4,209000070 -12669,4,203000110 -12670,5,297000100 -12671,5,291000020 -12672,5,297000130 -12673,5,297000140 -12674,5,203000010 -12675,5,206000030 -12676,5,203000050 -12677,5,202000090 -12678,5,204000080 -12679,3,170004 -12680,3,180004 -12681,4,140000 -12682,5,150010 -12683,5,150020 -12684,5,150030 -12685,5,150040 -12687,3,101000050 -12688,3,101000060 -12689,3,101000080 -12690,3,101000040 -12691,3,109000060 -12692,3,109000070 -12693,3,109000080 -12694,3,105000060 -12695,3,105000070 -12696,3,105000080 -12697,3,104000050 -12698,3,106000050 -12699,3,102000060 -12700,3,102000070 -12701,3,102000080 -12702,3,103000050 -12703,3,105000050 -12704,3,107000060 -12705,3,107000070 -12706,3,107000080 -12707,3,108000050 -12708,3,109000050 -12709,3,103000060 -12710,3,103000070 -12711,3,103000080 -12712,3,110000050 -12713,3,106000060 -12714,3,106000070 -12715,3,106000080 -12716,3,101000070 -12717,3,110000060 -12718,3,104000060 -12719,3,104000070 -12720,3,104000080 -12721,3,102000050 -12722,3,104000170 -12723,3,104000260 -12724,3,111000010 -12725,3,111000020 -12726,3,111000030 -12727,3,112000020 -12728,3,112000030 -12729,3,108000060 -12730,3,108000070 -12731,3,108000080 -12732,3,107000050 -12733,3,112000010 -12734,3,110000070 -12735,3,110000080 -12736,3,118000020 -12737,3,118000030 -12738,3,118000040 -12739,4,101000090 -12740,4,101000100 -12741,4,101000110 -12742,4,109000100 -12743,4,105000100 -12744,4,105000110 -12745,4,108000090 -12746,4,110000090 -12747,4,102000100 -12748,4,102000110 -12749,4,106000090 -12750,4,109000090 -12751,4,107000100 -12752,4,103000090 -12753,4,102000090 -12754,4,103000100 -12755,4,106000100 -12756,4,106000110 -12757,4,104000090 -12758,4,104000100 -12759,4,104000110 -12760,4,107000090 -12761,4,104000180 -12762,4,111000040 -12763,4,112000040 -12764,4,108000100 -12765,4,105000090 -12766,4,110000100 -12767,4,118000050 -12768,4,118000060 -12769,5,101000120 -12770,5,109000110 -12771,5,105000120 -12772,5,102000120 -12773,5,107000110 -12774,5,103000120 -12775,5,106000120 -12776,5,104000120 -12777,5,104000190 -12778,5,111000060 -12779,5,112000060 -12780,5,108000110 -12781,5,110000110 -12782,5,118000070 -12783,1,201000010 -12784,1,292000010 -12785,1,299000040 -12786,1,299000070 -12787,1,299000110 -12788,1,299000120 -12789,1,299000140 -12790,2,202000010 -12791,2,290000010 -12792,2,299000010 -12793,2,299000150 -12794,2,299000190 -12795,2,299000200 -12796,2,299000210 -12797,3,298000050 -12798,3,298000060 -12799,3,299000060 -12800,3,299000170 -12801,3,290000120 -12802,3,291000050 -12803,3,292000020 -12804,4,299000670 -12805,4,299000680 -12806,4,204000010 -12807,4,209000040 -12808,4,202000070 -12809,4,209000070 -12810,4,203000110 -12811,5,297000100 -12812,5,291000020 -12813,5,297000130 -12814,5,297000140 -12815,5,203000010 -12816,5,206000030 -12817,5,203000050 -12818,5,202000090 -12819,5,204000080 -12820,3,170004 -12821,3,180004 -12822,4,140000 -12823,5,150010 -12824,5,150020 -12825,5,150030 -12826,5,150040 -12828,3,101000050 -12829,3,101000060 -12830,3,101000080 -12831,3,101000040 -12832,3,109000060 -12833,3,109000070 -12834,3,109000080 -12835,3,105000060 -12836,3,105000070 -12837,3,105000080 -12838,3,104000050 -12839,3,106000050 -12840,3,102000060 -12841,3,102000070 -12842,3,102000080 -12843,3,103000050 -12844,3,105000050 -12845,3,107000060 -12846,3,107000070 -12847,3,107000080 -12848,3,108000050 -12849,3,109000050 -12850,3,103000060 -12851,3,103000070 -12852,3,103000080 -12853,3,110000050 -12854,3,106000060 -12855,3,106000070 -12856,3,106000080 -12857,3,101000070 -12858,3,110000060 -12859,3,104000060 -12860,3,104000070 -12861,3,104000080 -12862,3,102000050 -12863,3,104000170 -12864,3,104000260 -12865,3,111000010 -12866,3,111000020 -12867,3,111000030 -12868,3,112000020 -12869,3,112000030 -12870,3,108000060 -12871,3,108000070 -12872,3,108000080 -12873,3,107000050 -12874,3,112000010 -12875,3,110000070 -12876,3,110000080 -12877,3,118000020 -12878,3,118000030 -12879,3,118000040 -12880,4,101000090 -12881,4,101000100 -12882,4,101000110 -12883,4,109000100 -12884,4,105000100 -12885,4,105000110 -12886,4,108000090 -12887,4,110000090 -12888,4,102000100 -12889,4,102000110 -12890,4,106000090 -12891,4,109000090 -12892,4,107000100 -12893,4,103000090 -12894,4,102000090 -12895,4,103000100 -12896,4,106000100 -12897,4,106000110 -12898,4,104000090 -12899,4,104000100 -12900,4,104000110 -12901,4,107000090 -12902,4,104000180 -12903,4,111000040 -12904,4,112000040 -12905,4,108000100 -12906,4,105000090 -12907,4,110000100 -12908,4,118000050 -12909,4,118000060 -12910,5,101000120 -12911,5,109000110 -12912,5,105000120 -12913,5,102000120 -12914,5,107000110 -12915,5,103000120 -12916,5,106000120 -12917,5,104000120 -12918,5,104000190 -12919,5,111000060 -12920,5,112000060 -12921,5,108000110 -12922,5,110000110 -12923,5,118000070 -12924,1,201000010 -12925,1,292000010 -12926,1,299000040 -12927,1,299000070 -12928,1,299000110 -12929,1,299000120 -12930,1,299000140 -12931,2,202000010 -12932,2,290000010 -12933,2,299000010 -12934,2,299000150 -12935,2,299000190 -12936,2,299000200 -12937,2,299000210 -12938,3,298000050 -12939,3,298000060 -12940,3,299000060 -12941,3,299000170 -12942,3,290000120 -12943,3,291000050 -12944,3,292000020 -12945,4,299000670 -12946,4,299000680 -12947,4,204000010 -12948,4,209000040 -12949,4,202000070 -12950,4,209000070 -12951,4,203000110 -12952,5,297000100 -12953,5,291000020 -12954,5,297000130 -12955,5,297000140 -12956,5,203000010 -12957,5,206000030 -12958,5,203000050 -12959,5,202000090 -12960,5,204000080 -12961,3,170004 -12962,3,180004 -12963,4,140000 -12964,5,150010 -12965,5,150020 -12966,5,150030 -12967,5,150040 -12968,5,190000 -12969,5,200000 -12970,5,210000 -12972,3,102000060 -12973,3,102000070 -12974,3,102000080 -12975,3,103000050 -12976,3,105000050 -12977,3,107000060 -12978,3,107000070 -12979,3,107000080 -12980,3,108000050 -12981,3,109000050 -12982,3,103000060 -12983,3,103000070 -12984,3,103000080 -12985,3,110000050 -12986,4,102000100 -12987,4,102000110 -12988,4,102000350 -12989,4,106000090 -12990,4,109000090 -12991,4,107000100 -12992,4,103000090 -12993,4,102000090 -12994,4,103000100 -12995,5,102000120 -12996,5,107000110 -12997,5,103000120 -12998,1,102000000 -12999,2,102000001 -13000,3,102000002 -13001,4,102000003 -13002,5,102000004 -13003,1,105000000 -13004,2,105000001 -13005,3,105000002 -13006,4,105000003 -13007,5,105000004 -13008,1,112000000 -13009,2,112000001 -13010,3,112000002 -13011,4,112000003 -13012,5,112000004 -13013,3,170004 -13014,4,170005 -13015,3,180004 -13016,4,180005 -13017,4,140000 -13018,4,150010 -13019,4,150020 -13020,4,150030 -13021,4,150040 -13023,3,118000020 -13024,3,118000030 -13025,3,118000040 -13026,3,106000060 -13027,3,106000070 -13028,3,106000080 -13029,3,101000070 -13030,3,110000060 -13031,3,104000060 -13032,3,104000070 -13033,3,104000080 -13034,3,102000050 -13035,3,104000170 -13036,3,104000260 -13037,4,118000050 -13038,4,118000060 -13039,4,106000100 -13040,4,106000110 -13041,4,104000090 -13042,4,104000100 -13043,4,104000110 -13044,4,104000270 -13045,4,107000090 -13046,4,104000180 -13047,5,118000070 -13048,5,106000120 -13049,5,104000120 -13050,5,104000190 -13051,1,103000000 -13052,2,103000001 -13053,3,103000002 -13054,4,103000003 -13055,5,103000004 -13056,1,111000000 -13057,2,111000001 -13058,3,111000002 -13059,4,111000003 -13060,5,111000004 -13061,1,115000000 -13062,2,115000001 -13063,3,115000002 -13064,4,115000003 -13065,5,115000004 -13066,4,110024 -13067,4,110034 -13068,4,110044 -13069,4,110054 -13070,3,110060 -13071,3,110070 -13072,3,110080 -13073,3,110090 -13074,3,110100 -13075,3,110110 -13076,3,110120 -13077,3,110130 -13078,3,110140 -13079,3,110150 -13080,3,110160 -13081,3,110170 -13082,3,110180 -13083,3,110190 -13084,3,110200 -13085,3,110210 -13086,3,110220 -13087,3,110230 -13088,3,110240 -13089,3,110250 -13090,3,110260 -13091,3,110270 -13092,3,110620 -13093,3,110670 -13094,4,140000 -13095,4,150010 -13096,4,150020 -13097,4,150030 -13098,4,150040 -13100,3,111000010 -13101,3,111000020 -13102,3,111000030 -13103,3,112000020 -13104,3,112000030 -13105,3,108000060 -13106,3,108000070 -13107,3,108000080 -13108,3,107000050 -13109,3,112000010 -13110,3,110000070 -13111,3,110000080 -13112,4,111000040 -13113,4,112000040 -13114,4,108000100 -13115,4,105000090 -13116,4,110000100 -13117,5,111000060 -13118,5,112000060 -13119,5,108000110 -13120,5,110000110 -13121,1,108000000 -13122,2,108000001 -13123,3,108000002 -13124,4,108000003 -13125,5,108000004 -13126,1,107000000 -13127,2,107000001 -13128,3,107000002 -13129,4,107000003 -13130,5,107000004 -13131,1,120000000 -13132,2,120000001 -13133,3,120000002 -13134,4,120000003 -13135,5,120000004 -13136,4,120024 -13137,4,120034 -13138,4,120044 -13139,4,120054 -13140,3,120241 -13141,3,120251 -13142,3,120261 -13143,3,120271 -13144,3,120300 -13145,3,120310 -13146,3,120320 -13147,3,120330 -13148,3,120340 -13149,3,120350 -13150,3,120360 -13151,3,120370 -13152,3,120380 -13153,3,120390 -13154,3,120400 -13155,3,120410 -13156,3,120420 -13157,3,120430 -13158,3,120450 -13159,3,120460 -13160,3,120550 -13161,3,120560 -13162,3,120570 -13163,3,120990 -13164,3,121000 -13165,3,121010 -13166,3,121020 -13167,3,121100 -13168,4,140000 -13169,4,150010 -13170,4,150020 -13171,4,150030 -13172,4,150040 -13174,3,101000050 -13175,3,101000060 -13176,3,101000080 -13177,3,101000040 -13178,3,109000060 -13179,3,109000070 -13180,3,109000080 -13181,3,105000060 -13182,3,105000070 -13183,3,105000080 -13184,3,104000050 -13185,3,106000050 -13186,4,101000090 -13187,4,101000100 -13188,4,101000110 -13189,4,109000100 -13190,4,105000100 -13191,4,105000110 -13192,4,108000090 -13193,4,110000090 -13194,5,101000120 -13195,5,109000110 -13196,5,105000120 -13197,1,101000000 -13198,2,101000001 -13199,3,101000002 -13200,4,101000008 -13201,5,101000004 -13202,1,109000000 -13203,2,109000001 -13204,3,109000002 -13205,4,109000003 -13206,5,109000004 -13207,4,130024 -13208,4,130034 -13209,4,130044 -13210,4,130054 -13211,3,130060 -13212,3,130070 -13213,3,130080 -13214,3,130090 -13215,3,130100 -13216,3,130110 -13217,3,130120 -13218,3,130130 -13219,3,130140 -13220,3,130150 -13221,3,130160 -13222,3,130170 -13223,3,130180 -13224,3,130190 -13225,3,130200 -13226,3,130420 -13227,3,130510 -13228,3,130520 -13229,3,130531 -13230,3,130540 -13231,3,130660 -13232,3,130700 -13233,3,130790 -13234,3,130800 -13235,3,131130 -13236,4,140000 -13237,4,150010 -13238,4,150020 -13239,4,150030 -13240,4,150040 -13242,3,101000050 -13243,3,101000060 -13244,3,101000080 -13245,3,101000040 -13246,3,109000060 -13247,3,109000070 -13248,3,109000080 -13249,3,105000060 -13250,3,105000070 -13251,3,105000080 -13252,3,104000050 -13253,3,106000050 -13254,4,101000090 -13255,4,101000100 -13256,4,101000110 -13257,4,109000100 -13258,4,105000100 -13259,4,105000110 -13260,4,108000090 -13261,4,110000090 -13262,5,101000120 -13263,5,109000110 -13264,5,105000120 -13265,1,101000000 -13266,2,101000001 -13267,3,101000002 -13268,4,101000008 -13269,5,101000004 -13270,1,109000000 -13271,2,109000001 -13272,3,109000002 -13273,4,109000003 -13274,5,109000004 -13275,3,170004 -13276,4,170005 -13277,3,180004 -13278,4,180005 -13279,4,140000 -13280,4,150010 -13281,4,150020 -13282,4,150030 -13283,4,150040 -13285,3,102000060 -13286,3,102000070 -13287,3,102000080 -13288,3,103000050 -13289,3,105000050 -13290,3,107000060 -13291,3,107000070 -13292,3,107000080 -13293,3,108000050 -13294,3,109000050 -13295,3,103000060 -13296,3,103000070 -13297,3,103000080 -13298,3,110000050 -13299,4,102000100 -13300,4,102000110 -13301,4,102000350 -13302,4,106000090 -13303,4,109000090 -13304,4,107000100 -13305,4,103000090 -13306,4,102000090 -13307,4,103000100 -13308,5,102000120 -13309,5,107000110 -13310,5,103000120 -13311,1,102000000 -13312,2,102000001 -13313,3,102000002 -13314,4,102000003 -13315,5,102000004 -13316,1,105000000 -13317,2,105000001 -13318,3,105000002 -13319,4,105000003 -13320,5,105000004 -13321,1,112000000 -13322,2,112000001 -13323,3,112000002 -13324,4,112000003 -13325,5,112000004 -13326,4,110024 -13327,4,110034 -13328,4,110044 -13329,4,110054 -13330,3,110060 -13331,3,110070 -13332,3,110080 -13333,3,110090 -13334,3,110100 -13335,3,110110 -13336,3,110120 -13337,3,110130 -13338,3,110140 -13339,3,110150 -13340,3,110160 -13341,3,110170 -13342,3,110180 -13343,3,110190 -13344,3,110200 -13345,3,110210 -13346,3,110220 -13347,3,110230 -13348,3,110240 -13349,3,110250 -13350,3,110260 -13351,3,110270 -13352,3,110620 -13353,3,110670 -13354,4,140000 -13355,4,150010 -13356,4,150020 -13357,4,150030 -13358,4,150040 -13360,3,118000020 -13361,3,118000030 -13362,3,118000040 -13363,3,106000060 -13364,3,106000070 -13365,3,106000080 -13366,3,101000070 -13367,3,110000060 -13368,3,104000060 -13369,3,104000070 -13370,3,104000080 -13371,3,102000050 -13372,3,104000170 -13373,3,104000260 -13374,4,118000050 -13375,4,118000060 -13376,4,106000100 -13377,4,106000110 -13378,4,104000090 -13379,4,104000100 -13380,4,104000110 -13381,4,104000270 -13382,4,107000090 -13383,4,104000180 -13384,5,118000070 -13385,5,106000120 -13386,5,104000120 -13387,5,104000190 -13388,1,103000000 -13389,2,103000001 -13390,3,103000002 -13391,4,103000003 -13392,5,103000004 -13393,1,111000000 -13394,2,111000001 -13395,3,111000002 -13396,4,111000003 -13397,5,111000004 -13398,1,115000000 -13399,2,115000001 -13400,3,115000002 -13401,4,115000003 -13402,5,115000004 -13403,4,120024 -13404,4,120034 -13405,4,120044 -13406,4,120054 -13407,3,120241 -13408,3,120251 -13409,3,120261 -13410,3,120271 -13411,3,120300 -13412,3,120310 -13413,3,120320 -13414,3,120330 -13415,3,120340 -13416,3,120350 -13417,3,120360 -13418,3,120370 -13419,3,120380 -13420,3,120390 -13421,3,120400 -13422,3,120410 -13423,3,120420 -13424,3,120430 -13425,3,120450 -13426,3,120460 -13427,3,120550 -13428,3,120560 -13429,3,120570 -13430,3,120990 -13431,3,121000 -13432,3,121010 -13433,3,121020 -13434,3,121100 -13435,4,140000 -13436,4,150010 -13437,4,150020 -13438,4,150030 -13439,4,150040 -13441,3,111000010 -13442,3,111000020 -13443,3,111000030 -13444,3,112000020 -13445,3,112000030 -13446,3,108000060 -13447,3,108000070 -13448,3,108000080 -13449,3,107000050 -13450,3,112000010 -13451,3,110000070 -13452,3,110000080 -13453,4,111000040 -13454,4,112000040 -13455,4,108000100 -13456,4,105000090 -13457,4,110000100 -13458,5,111000060 -13459,5,112000060 -13460,5,108000110 -13461,5,110000110 -13462,1,108000000 -13463,2,108000001 -13464,3,108000002 -13465,4,108000003 -13466,5,108000004 -13467,1,107000000 -13468,2,107000001 -13469,3,107000002 -13470,4,107000003 -13471,5,107000004 -13472,1,120000000 -13473,2,120000001 -13474,3,120000002 -13475,4,120000003 -13476,5,120000004 -13477,4,130024 -13478,4,130034 -13479,4,130044 -13480,4,130054 -13481,3,130060 -13482,3,130070 -13483,3,130080 -13484,3,130090 -13485,3,130100 -13486,3,130110 -13487,3,130120 -13488,3,130130 -13489,3,130140 -13490,3,130150 -13491,3,130160 -13492,3,130170 -13493,3,130180 -13494,3,130190 -13495,3,130200 -13496,3,130420 -13497,3,130510 -13498,3,130520 -13499,3,130531 -13500,3,130540 -13501,3,130660 -13502,3,130700 -13503,3,130790 -13504,3,130800 -13505,3,131130 -13506,4,140000 -13507,4,150010 -13508,4,150020 -13509,4,150030 -13510,4,150040 -13512,1,101000010 -13513,1,102000010 -13514,1,103000010 -13515,1,104000010 -13516,1,105000010 -13517,1,106000010 -13518,1,107000010 -13519,1,108000010 -13520,1,109000010 -13521,1,110000010 -13522,2,101000020 -13523,2,101000030 -13524,2,102000020 -13525,2,102000030 -13526,2,102000040 -13527,2,103000020 -13528,2,103000030 -13529,2,103000040 -13530,2,104000020 -13531,2,104000030 -13532,2,104000040 -13533,2,105000020 -13534,2,105000030 -13535,2,105000040 -13536,2,106000020 -13537,2,106000030 -13538,2,106000040 -13539,2,107000020 -13540,2,107000030 -13541,2,107000040 -13542,2,108000020 -13543,2,108000030 -13544,2,108000040 -13545,2,109000020 -13546,2,109000030 -13547,2,109000040 -13548,2,110000020 -13549,2,110000030 -13550,2,110000040 -13551,2,118000010 -13552,3,101000050 -13553,3,101000060 -13554,3,101000080 -13555,3,101000040 -13556,3,109000060 -13557,3,109000070 -13558,3,109000080 -13559,3,105000060 -13560,3,105000070 -13561,3,105000080 -13562,3,104000050 -13563,3,106000050 -13564,3,102000060 -13565,3,102000070 -13566,3,102000080 -13567,3,103000050 -13568,3,105000050 -13569,3,107000060 -13570,3,107000070 -13571,3,107000080 -13572,3,108000050 -13573,3,109000050 -13574,3,103000060 -13575,3,103000070 -13576,3,103000080 -13577,3,110000050 -13578,3,106000060 -13579,3,106000070 -13580,3,106000080 -13581,3,101000070 -13582,3,110000060 -13583,3,104000060 -13584,3,104000070 -13585,3,104000080 -13586,3,102000050 -13587,3,104000170 -13588,3,104000260 -13589,3,111000010 -13590,3,111000020 -13591,3,111000030 -13592,3,112000020 -13593,3,112000030 -13594,3,108000060 -13595,3,108000070 -13596,3,108000080 -13597,3,107000050 -13598,3,112000010 -13599,3,110000070 -13600,3,110000080 -13601,3,118000020 -13602,3,118000030 -13603,3,118000040 -13604,4,101000090 -13605,4,101000100 -13606,4,101000110 -13607,4,109000100 -13608,4,105000100 -13609,4,105000110 -13610,4,108000090 -13611,4,110000090 -13612,4,102000100 -13613,4,102000110 -13614,4,106000090 -13615,4,109000090 -13616,4,107000100 -13617,4,103000090 -13618,4,102000090 -13619,4,103000100 -13620,4,106000100 -13621,4,106000110 -13622,4,104000090 -13623,4,104000100 -13624,4,104000110 -13625,4,107000090 -13626,4,104000180 -13627,4,111000040 -13628,4,112000040 -13629,4,108000100 -13630,4,105000090 -13631,4,110000100 -13632,4,118000050 -13633,4,118000060 -13634,5,101000120 -13635,5,109000110 -13636,5,105000120 -13637,5,102000120 -13638,5,107000110 -13639,5,103000120 -13640,5,106000120 -13641,5,104000120 -13642,5,104000190 -13643,5,111000060 -13644,5,112000060 -13645,5,108000110 -13646,5,110000110 -13647,5,118000070 -13648,1,201000010 -13649,1,292000010 -13650,1,299000040 -13651,1,299000070 -13652,1,299000110 -13653,1,299000120 -13654,1,299000140 -13655,2,202000010 -13656,2,290000010 -13657,2,299000010 -13658,2,299000150 -13659,2,299000190 -13660,2,299000200 -13661,2,299000210 -13662,3,298000050 -13663,3,298000060 -13664,3,299000060 -13665,3,299000170 -13666,3,290000120 -13667,3,291000050 -13668,3,292000020 -13669,4,299000670 -13670,4,299000680 -13671,4,204000010 -13672,4,209000040 -13673,4,202000070 -13674,4,209000070 -13675,4,203000110 -13676,4,290000110 -13677,5,297000100 -13678,5,291000020 -13679,5,297000130 -13680,5,297000140 -13681,5,203000010 -13682,5,206000030 -13683,5,203000050 -13684,5,202000090 -13685,5,204000080 -13686,5,202000150 -13687,1,170002 -13688,1,180002 -13689,2,170003 -13690,2,180003 -13691,3,170004 -13692,3,180004 -13693,4,140000 -13694,5,150010 -13695,5,150020 -13696,5,150030 -13697,5,150040 -13699,1,101000010 -13700,1,102000010 -13701,1,103000010 -13702,1,104000010 -13703,1,105000010 -13704,1,106000010 -13705,1,107000010 -13706,1,108000010 -13707,1,109000010 -13708,1,110000010 -13709,2,101000020 -13710,2,101000030 -13711,2,102000020 -13712,2,102000030 -13713,2,102000040 -13714,2,103000020 -13715,2,103000030 -13716,2,103000040 -13717,2,104000020 -13718,2,104000030 -13719,2,104000040 -13720,2,105000020 -13721,2,105000030 -13722,2,105000040 -13723,2,106000020 -13724,2,106000030 -13725,2,106000040 -13726,2,107000020 -13727,2,107000030 -13728,2,107000040 -13729,2,108000020 -13730,2,108000030 -13731,2,108000040 -13732,2,109000020 -13733,2,109000030 -13734,2,109000040 -13735,2,110000020 -13736,2,110000030 -13737,2,110000040 -13738,2,118000010 -13739,3,101000050 -13740,3,101000060 -13741,3,101000080 -13742,3,101000040 -13743,3,109000060 -13744,3,109000070 -13745,3,109000080 -13746,3,105000060 -13747,3,105000070 -13748,3,105000080 -13749,3,104000050 -13750,3,106000050 -13751,3,102000060 -13752,3,102000070 -13753,3,102000080 -13754,3,103000050 -13755,3,105000050 -13756,3,107000060 -13757,3,107000070 -13758,3,107000080 -13759,3,108000050 -13760,3,109000050 -13761,3,103000060 -13762,3,103000070 -13763,3,103000080 -13764,3,110000050 -13765,3,106000060 -13766,3,106000070 -13767,3,106000080 -13768,3,101000070 -13769,3,110000060 -13770,3,104000060 -13771,3,104000070 -13772,3,104000080 -13773,3,102000050 -13774,3,104000170 -13775,3,104000260 -13776,3,111000010 -13777,3,111000020 -13778,3,111000030 -13779,3,112000020 -13780,3,112000030 -13781,3,108000060 -13782,3,108000070 -13783,3,108000080 -13784,3,107000050 -13785,3,112000010 -13786,3,110000070 -13787,3,110000080 -13788,3,118000020 -13789,3,118000030 -13790,3,118000040 -13791,4,101000090 -13792,4,101000100 -13793,4,101000110 -13794,4,109000100 -13795,4,105000100 -13796,4,105000110 -13797,4,108000090 -13798,4,110000090 -13799,4,102000100 -13800,4,102000110 -13801,4,106000090 -13802,4,109000090 -13803,4,107000100 -13804,4,103000090 -13805,4,102000090 -13806,4,103000100 -13807,4,106000100 -13808,4,106000110 -13809,4,104000090 -13810,4,104000100 -13811,4,104000110 -13812,4,107000090 -13813,4,104000180 -13814,4,111000040 -13815,4,112000040 -13816,4,108000100 -13817,4,105000090 -13818,4,110000100 -13819,4,118000050 -13820,4,118000060 -13821,5,101000120 -13822,5,109000110 -13823,5,105000120 -13824,5,102000120 -13825,5,107000110 -13826,5,103000120 -13827,5,106000120 -13828,5,104000120 -13829,5,104000190 -13830,5,111000060 -13831,5,112000060 -13832,5,108000110 -13833,5,110000110 -13834,5,118000070 -13835,1,201000010 -13836,1,292000010 -13837,1,299000040 -13838,1,299000070 -13839,1,299000110 -13840,1,299000120 -13841,1,299000140 -13842,2,202000010 -13843,2,290000010 -13844,2,299000010 -13845,2,299000150 -13846,2,299000190 -13847,2,299000200 -13848,2,299000210 -13849,3,298000050 -13850,3,298000060 -13851,3,299000060 -13852,3,299000170 -13853,3,290000120 -13854,3,291000050 -13855,3,292000020 -13856,4,299000670 -13857,4,299000680 -13858,4,204000010 -13859,4,209000040 -13860,4,202000070 -13861,4,209000070 -13862,4,203000110 -13863,4,290000110 -13864,5,297000100 -13865,5,291000020 -13866,5,297000130 -13867,5,297000140 -13868,5,203000010 -13869,5,206000030 -13870,5,203000050 -13871,5,202000090 -13872,5,204000080 -13873,5,202000150 -13874,2,170003 -13875,2,180003 -13876,3,170004 -13877,3,180004 -13878,4,140000 -13879,5,150010 -13880,5,150020 -13881,5,150030 -13882,5,150040 -13884,3,101000050 -13885,3,101000060 -13886,3,101000080 -13887,3,101000040 -13888,3,109000060 -13889,3,109000070 -13890,3,109000080 -13891,3,105000060 -13892,3,105000070 -13893,3,105000080 -13894,3,104000050 -13895,3,106000050 -13896,3,102000060 -13897,3,102000070 -13898,3,102000080 -13899,3,103000050 -13900,3,105000050 -13901,3,107000060 -13902,3,107000070 -13903,3,107000080 -13904,3,108000050 -13905,3,109000050 -13906,3,103000060 -13907,3,103000070 -13908,3,103000080 -13909,3,110000050 -13910,3,106000060 -13911,3,106000070 -13912,3,106000080 -13913,3,101000070 -13914,3,110000060 -13915,3,104000060 -13916,3,104000070 -13917,3,104000080 -13918,3,102000050 -13919,3,104000170 -13920,3,104000260 -13921,3,111000010 -13922,3,111000020 -13923,3,111000030 -13924,3,112000020 -13925,3,112000030 -13926,3,108000060 -13927,3,108000070 -13928,3,108000080 -13929,3,107000050 -13930,3,112000010 -13931,3,110000070 -13932,3,110000080 -13933,3,118000020 -13934,3,118000030 -13935,3,118000040 -13936,4,101000090 -13937,4,101000100 -13938,4,101000110 -13939,4,109000100 -13940,4,105000100 -13941,4,105000110 -13942,4,108000090 -13943,4,110000090 -13944,4,102000100 -13945,4,102000110 -13946,4,106000090 -13947,4,109000090 -13948,4,107000100 -13949,4,103000090 -13950,4,102000090 -13951,4,103000100 -13952,4,106000100 -13953,4,106000110 -13954,4,104000090 -13955,4,104000100 -13956,4,104000110 -13957,4,107000090 -13958,4,104000180 -13959,4,111000040 -13960,4,112000040 -13961,4,108000100 -13962,4,105000090 -13963,4,110000100 -13964,4,118000050 -13965,4,118000060 -13966,5,101000120 -13967,5,109000110 -13968,5,105000120 -13969,5,102000120 -13970,5,107000110 -13971,5,103000120 -13972,5,106000120 -13973,5,104000120 -13974,5,104000190 -13975,5,111000060 -13976,5,112000060 -13977,5,108000110 -13978,5,110000110 -13979,5,118000070 -13980,1,201000010 -13981,1,292000010 -13982,1,299000040 -13983,1,299000070 -13984,1,299000110 -13985,1,299000120 -13986,1,299000140 -13987,2,202000010 -13988,2,290000010 -13989,2,299000010 -13990,2,299000150 -13991,2,299000190 -13992,2,299000200 -13993,2,299000210 -13994,3,298000050 -13995,3,298000060 -13996,3,299000060 -13997,3,299000170 -13998,3,290000120 -13999,3,291000050 -14000,3,292000020 -14001,4,299000670 -14002,4,299000680 -14003,4,204000010 -14004,4,209000040 -14005,4,202000070 -14006,4,209000070 -14007,4,203000110 -14008,4,290000110 -14009,5,297000100 -14010,5,291000020 -14011,5,297000130 -14012,5,297000140 -14013,5,203000010 -14014,5,206000030 -14015,5,203000050 -14016,5,202000090 -14017,5,204000080 -14018,5,202000150 -14019,3,170004 -14020,3,180004 -14021,4,140000 -14022,5,150010 -14023,5,150020 -14024,5,150030 -14025,5,150040 -14027,3,101000050 -14028,3,101000060 -14029,3,101000080 -14030,3,101000040 -14031,3,109000060 -14032,3,109000070 -14033,3,109000080 -14034,3,105000060 -14035,3,105000070 -14036,3,105000080 -14037,3,104000050 -14038,3,106000050 -14039,3,102000060 -14040,3,102000070 -14041,3,102000080 -14042,3,103000050 -14043,3,105000050 -14044,3,107000060 -14045,3,107000070 -14046,3,107000080 -14047,3,108000050 -14048,3,109000050 -14049,3,103000060 -14050,3,103000070 -14051,3,103000080 -14052,3,110000050 -14053,3,106000060 -14054,3,106000070 -14055,3,106000080 -14056,3,101000070 -14057,3,110000060 -14058,3,104000060 -14059,3,104000070 -14060,3,104000080 -14061,3,102000050 -14062,3,104000170 -14063,3,104000260 -14064,3,111000010 -14065,3,111000020 -14066,3,111000030 -14067,3,112000020 -14068,3,112000030 -14069,3,108000060 -14070,3,108000070 -14071,3,108000080 -14072,3,107000050 -14073,3,112000010 -14074,3,110000070 -14075,3,110000080 -14076,3,118000020 -14077,3,118000030 -14078,3,118000040 -14079,4,101000090 -14080,4,101000100 -14081,4,101000110 -14082,4,109000100 -14083,4,105000100 -14084,4,105000110 -14085,4,108000090 -14086,4,110000090 -14087,4,102000100 -14088,4,102000110 -14089,4,106000090 -14090,4,109000090 -14091,4,107000100 -14092,4,103000090 -14093,4,102000090 -14094,4,103000100 -14095,4,106000100 -14096,4,106000110 -14097,4,104000090 -14098,4,104000100 -14099,4,104000110 -14100,4,107000090 -14101,4,104000180 -14102,4,111000040 -14103,4,112000040 -14104,4,108000100 -14105,4,105000090 -14106,4,110000100 -14107,4,118000050 -14108,4,118000060 -14109,5,101000120 -14110,5,109000110 -14111,5,105000120 -14112,5,102000120 -14113,5,107000110 -14114,5,103000120 -14115,5,106000120 -14116,5,104000120 -14117,5,104000190 -14118,5,111000060 -14119,5,112000060 -14120,5,108000110 -14121,5,110000110 -14122,5,118000070 -14123,1,201000010 -14124,1,292000010 -14125,1,299000040 -14126,1,299000070 -14127,1,299000110 -14128,1,299000120 -14129,1,299000140 -14130,2,202000010 -14131,2,290000010 -14132,2,299000010 -14133,2,299000150 -14134,2,299000190 -14135,2,299000200 -14136,2,299000210 -14137,3,298000050 -14138,3,298000060 -14139,3,299000060 -14140,3,299000170 -14141,3,290000120 -14142,3,291000050 -14143,3,292000020 -14144,4,299000670 -14145,4,299000680 -14146,4,204000010 -14147,4,209000040 -14148,4,202000070 -14149,4,209000070 -14150,4,203000110 -14151,4,290000110 -14152,5,297000100 -14153,5,291000020 -14154,5,297000130 -14155,5,297000140 -14156,5,203000010 -14157,5,206000030 -14158,5,203000050 -14159,5,202000090 -14160,5,204000080 -14161,5,202000150 -14162,3,170004 -14163,3,180004 -14164,4,140000 -14165,5,150010 -14166,5,150020 -14167,5,150030 -14168,5,150040 -14170,3,101000050 -14171,3,101000060 -14172,3,101000080 -14173,3,101000040 -14174,3,109000060 -14175,3,109000070 -14176,3,109000080 -14177,3,105000060 -14178,3,105000070 -14179,3,105000080 -14180,3,104000050 -14181,3,106000050 -14182,3,102000060 -14183,3,102000070 -14184,3,102000080 -14185,3,103000050 -14186,3,105000050 -14187,3,107000060 -14188,3,107000070 -14189,3,107000080 -14190,3,108000050 -14191,3,109000050 -14192,3,103000060 -14193,3,103000070 -14194,3,103000080 -14195,3,110000050 -14196,3,106000060 -14197,3,106000070 -14198,3,106000080 -14199,3,101000070 -14200,3,110000060 -14201,3,104000060 -14202,3,104000070 -14203,3,104000080 -14204,3,102000050 -14205,3,104000170 -14206,3,104000260 -14207,3,111000010 -14208,3,111000020 -14209,3,111000030 -14210,3,112000020 -14211,3,112000030 -14212,3,108000060 -14213,3,108000070 -14214,3,108000080 -14215,3,107000050 -14216,3,112000010 -14217,3,110000070 -14218,3,110000080 -14219,3,118000020 -14220,3,118000030 -14221,3,118000040 -14222,4,101000090 -14223,4,101000100 -14224,4,101000110 -14225,4,109000100 -14226,4,105000100 -14227,4,105000110 -14228,4,108000090 -14229,4,110000090 -14230,4,102000100 -14231,4,102000110 -14232,4,106000090 -14233,4,109000090 -14234,4,107000100 -14235,4,103000090 -14236,4,102000090 -14237,4,103000100 -14238,4,106000100 -14239,4,106000110 -14240,4,104000090 -14241,4,104000100 -14242,4,104000110 -14243,4,107000090 -14244,4,104000180 -14245,4,111000040 -14246,4,112000040 -14247,4,108000100 -14248,4,105000090 -14249,4,110000100 -14250,4,118000050 -14251,4,118000060 -14252,5,101000120 -14253,5,109000110 -14254,5,105000120 -14255,5,102000120 -14256,5,107000110 -14257,5,103000120 -14258,5,106000120 -14259,5,104000120 -14260,5,104000190 -14261,5,111000060 -14262,5,112000060 -14263,5,108000110 -14264,5,110000110 -14265,5,118000070 -14266,1,201000010 -14267,1,292000010 -14268,1,299000040 -14269,1,299000070 -14270,1,299000110 -14271,1,299000120 -14272,1,299000140 -14273,2,202000010 -14274,2,290000010 -14275,2,299000010 -14276,2,299000150 -14277,2,299000190 -14278,2,299000200 -14279,2,299000210 -14280,3,298000050 -14281,3,298000060 -14282,3,299000060 -14283,3,299000170 -14284,3,290000120 -14285,3,291000050 -14286,3,292000020 -14287,4,299000670 -14288,4,299000680 -14289,4,204000010 -14290,4,209000040 -14291,4,202000070 -14292,4,209000070 -14293,4,203000110 -14294,4,290000110 -14295,5,297000100 -14296,5,291000020 -14297,5,297000130 -14298,5,297000140 -14299,5,203000010 -14300,5,206000030 -14301,5,203000050 -14302,5,202000090 -14303,5,204000080 -14304,5,202000150 -14305,3,170004 -14306,3,180004 -14307,4,140000 -14308,5,150010 -14309,5,150020 -14310,5,150030 -14311,5,150040 -14312,5,190000 -14313,5,200000 -14314,5,210000 -14316,1,101000010 -14317,1,102000010 -14318,1,103000010 -14319,1,104000010 -14320,1,105000010 -14321,1,106000010 -14322,1,107000010 -14323,1,108000010 -14324,1,109000010 -14325,1,110000010 -14326,2,101000020 -14327,2,101000030 -14328,2,102000020 -14329,2,102000030 -14330,2,102000040 -14331,2,103000020 -14332,2,103000030 -14333,2,103000040 -14334,2,104000020 -14335,2,104000030 -14336,2,104000040 -14337,2,105000020 -14338,2,105000030 -14339,2,105000040 -14340,2,106000020 -14341,2,106000030 -14342,2,106000040 -14343,2,107000020 -14344,2,107000030 -14345,2,107000040 -14346,2,108000020 -14347,2,108000030 -14348,2,108000040 -14349,2,109000020 -14350,2,109000030 -14351,2,109000040 -14352,2,110000020 -14353,2,110000030 -14354,2,110000040 -14355,2,118000010 -14356,3,101000050 -14357,3,101000060 -14358,3,101000080 -14359,3,101000040 -14360,3,109000060 -14361,3,109000070 -14362,3,109000080 -14363,3,105000060 -14364,3,105000070 -14365,3,105000080 -14366,3,104000050 -14367,3,106000050 -14368,3,102000060 -14369,3,102000070 -14370,3,102000080 -14371,3,103000050 -14372,3,105000050 -14373,3,107000060 -14374,3,107000070 -14375,3,107000080 -14376,3,108000050 -14377,3,109000050 -14378,3,103000060 -14379,3,103000070 -14380,3,103000080 -14381,3,110000050 -14382,3,106000060 -14383,3,106000070 -14384,3,106000080 -14385,3,101000070 -14386,3,110000060 -14387,3,104000060 -14388,3,104000070 -14389,3,104000080 -14390,3,102000050 -14391,3,104000170 -14392,3,104000260 -14393,3,111000010 -14394,3,111000020 -14395,3,111000030 -14396,3,112000020 -14397,3,112000030 -14398,3,108000060 -14399,3,108000070 -14400,3,108000080 -14401,3,107000050 -14402,3,112000010 -14403,3,110000070 -14404,3,110000080 -14405,3,118000020 -14406,3,118000030 -14407,3,118000040 -14408,4,101000090 -14409,4,101000100 -14410,4,101000110 -14411,4,109000100 -14412,4,105000100 -14413,4,105000110 -14414,4,108000090 -14415,4,110000090 -14416,4,102000100 -14417,4,102000110 -14418,4,106000090 -14419,4,109000090 -14420,4,107000100 -14421,4,103000090 -14422,4,102000090 -14423,4,103000100 -14424,4,106000100 -14425,4,106000110 -14426,4,104000090 -14427,4,104000100 -14428,4,104000110 -14429,4,107000090 -14430,4,104000180 -14431,4,111000040 -14432,4,112000040 -14433,4,108000100 -14434,4,105000090 -14435,4,110000100 -14436,4,118000050 -14437,4,118000060 -14438,5,101000120 -14439,5,109000110 -14440,5,105000120 -14441,5,102000120 -14442,5,107000110 -14443,5,103000120 -14444,5,106000120 -14445,5,104000120 -14446,5,104000190 -14447,5,111000060 -14448,5,112000060 -14449,5,108000110 -14450,5,110000110 -14451,5,118000070 -14452,1,201000010 -14453,1,292000010 -14454,1,299000040 -14455,1,299000070 -14456,1,299000110 -14457,1,299000120 -14458,1,299000140 -14459,2,202000010 -14460,2,290000010 -14461,2,299000010 -14462,2,299000150 -14463,2,299000190 -14464,2,299000200 -14465,2,299000210 -14466,3,298000050 -14467,3,298000060 -14468,3,299000060 -14469,3,299000170 -14470,3,290000120 -14471,3,291000050 -14472,3,292000020 -14473,4,299000670 -14474,4,299000680 -14475,4,204000010 -14476,4,209000040 -14477,4,202000070 -14478,4,209000070 -14479,4,203000110 -14480,4,290000110 -14481,4,206000110 -14482,5,297000100 -14483,5,291000020 -14484,5,297000130 -14485,5,297000140 -14486,5,203000010 -14487,5,206000030 -14488,5,203000050 -14489,5,202000090 -14490,5,204000080 -14491,5,202000150 -14492,5,204000100 -14493,1,170002 -14494,1,180002 -14495,2,170003 -14496,2,180003 -14497,3,170004 -14498,3,180004 -14499,4,140000 -14500,5,150010 -14501,5,150020 -14502,5,150030 -14503,5,150040 -14505,1,101000010 -14506,1,102000010 -14507,1,103000010 -14508,1,104000010 -14509,1,105000010 -14510,1,106000010 -14511,1,107000010 -14512,1,108000010 -14513,1,109000010 -14514,1,110000010 -14515,2,101000020 -14516,2,101000030 -14517,2,102000020 -14518,2,102000030 -14519,2,102000040 -14520,2,103000020 -14521,2,103000030 -14522,2,103000040 -14523,2,104000020 -14524,2,104000030 -14525,2,104000040 -14526,2,105000020 -14527,2,105000030 -14528,2,105000040 -14529,2,106000020 -14530,2,106000030 -14531,2,106000040 -14532,2,107000020 -14533,2,107000030 -14534,2,107000040 -14535,2,108000020 -14536,2,108000030 -14537,2,108000040 -14538,2,109000020 -14539,2,109000030 -14540,2,109000040 -14541,2,110000020 -14542,2,110000030 -14543,2,110000040 -14544,2,118000010 -14545,3,101000050 -14546,3,101000060 -14547,3,101000080 -14548,3,101000040 -14549,3,109000060 -14550,3,109000070 -14551,3,109000080 -14552,3,105000060 -14553,3,105000070 -14554,3,105000080 -14555,3,104000050 -14556,3,106000050 -14557,3,102000060 -14558,3,102000070 -14559,3,102000080 -14560,3,103000050 -14561,3,105000050 -14562,3,107000060 -14563,3,107000070 -14564,3,107000080 -14565,3,108000050 -14566,3,109000050 -14567,3,103000060 -14568,3,103000070 -14569,3,103000080 -14570,3,110000050 -14571,3,106000060 -14572,3,106000070 -14573,3,106000080 -14574,3,101000070 -14575,3,110000060 -14576,3,104000060 -14577,3,104000070 -14578,3,104000080 -14579,3,102000050 -14580,3,104000170 -14581,3,104000260 -14582,3,111000010 -14583,3,111000020 -14584,3,111000030 -14585,3,112000020 -14586,3,112000030 -14587,3,108000060 -14588,3,108000070 -14589,3,108000080 -14590,3,107000050 -14591,3,112000010 -14592,3,110000070 -14593,3,110000080 -14594,3,118000020 -14595,3,118000030 -14596,3,118000040 -14597,4,101000090 -14598,4,101000100 -14599,4,101000110 -14600,4,109000100 -14601,4,105000100 -14602,4,105000110 -14603,4,108000090 -14604,4,110000090 -14605,4,102000100 -14606,4,102000110 -14607,4,106000090 -14608,4,109000090 -14609,4,107000100 -14610,4,103000090 -14611,4,102000090 -14612,4,103000100 -14613,4,106000100 -14614,4,106000110 -14615,4,104000090 -14616,4,104000100 -14617,4,104000110 -14618,4,107000090 -14619,4,104000180 -14620,4,111000040 -14621,4,112000040 -14622,4,108000100 -14623,4,105000090 -14624,4,110000100 -14625,4,118000050 -14626,4,118000060 -14627,5,101000120 -14628,5,109000110 -14629,5,105000120 -14630,5,102000120 -14631,5,107000110 -14632,5,103000120 -14633,5,106000120 -14634,5,104000120 -14635,5,104000190 -14636,5,111000060 -14637,5,112000060 -14638,5,108000110 -14639,5,110000110 -14640,5,118000070 -14641,1,201000010 -14642,1,292000010 -14643,1,299000040 -14644,1,299000070 -14645,1,299000110 -14646,1,299000120 -14647,1,299000140 -14648,2,202000010 -14649,2,290000010 -14650,2,299000010 -14651,2,299000150 -14652,2,299000190 -14653,2,299000200 -14654,2,299000210 -14655,3,298000050 -14656,3,298000060 -14657,3,299000060 -14658,3,299000170 -14659,3,290000120 -14660,3,291000050 -14661,3,292000020 -14662,4,299000670 -14663,4,299000680 -14664,4,204000010 -14665,4,209000040 -14666,4,202000070 -14667,4,209000070 -14668,4,203000110 -14669,4,290000110 -14670,4,206000110 -14671,5,297000100 -14672,5,291000020 -14673,5,297000130 -14674,5,297000140 -14675,5,203000010 -14676,5,206000030 -14677,5,203000050 -14678,5,202000090 -14679,5,204000080 -14680,5,202000150 -14681,5,204000100 -14682,2,170003 -14683,2,180003 -14684,3,170004 -14685,3,180004 -14686,4,140000 -14687,5,150010 -14688,5,150020 -14689,5,150030 -14690,5,150040 -14692,3,101000050 -14693,3,101000060 -14694,3,101000080 -14695,3,101000040 -14696,3,109000060 -14697,3,109000070 -14698,3,109000080 -14699,3,105000060 -14700,3,105000070 -14701,3,105000080 -14702,3,104000050 -14703,3,106000050 -14704,3,102000060 -14705,3,102000070 -14706,3,102000080 -14707,3,103000050 -14708,3,105000050 -14709,3,107000060 -14710,3,107000070 -14711,3,107000080 -14712,3,108000050 -14713,3,109000050 -14714,3,103000060 -14715,3,103000070 -14716,3,103000080 -14717,3,110000050 -14718,3,106000060 -14719,3,106000070 -14720,3,106000080 -14721,3,101000070 -14722,3,110000060 -14723,3,104000060 -14724,3,104000070 -14725,3,104000080 -14726,3,102000050 -14727,3,104000170 -14728,3,104000260 -14729,3,111000010 -14730,3,111000020 -14731,3,111000030 -14732,3,112000020 -14733,3,112000030 -14734,3,108000060 -14735,3,108000070 -14736,3,108000080 -14737,3,107000050 -14738,3,112000010 -14739,3,110000070 -14740,3,110000080 -14741,3,118000020 -14742,3,118000030 -14743,3,118000040 -14744,4,101000090 -14745,4,101000100 -14746,4,101000110 -14747,4,109000100 -14748,4,105000100 -14749,4,105000110 -14750,4,108000090 -14751,4,110000090 -14752,4,102000100 -14753,4,102000110 -14754,4,106000090 -14755,4,109000090 -14756,4,107000100 -14757,4,103000090 -14758,4,102000090 -14759,4,103000100 -14760,4,106000100 -14761,4,106000110 -14762,4,104000090 -14763,4,104000100 -14764,4,104000110 -14765,4,107000090 -14766,4,104000180 -14767,4,111000040 -14768,4,112000040 -14769,4,108000100 -14770,4,105000090 -14771,4,110000100 -14772,4,118000050 -14773,4,118000060 -14774,5,101000120 -14775,5,109000110 -14776,5,105000120 -14777,5,102000120 -14778,5,107000110 -14779,5,103000120 -14780,5,106000120 -14781,5,104000120 -14782,5,104000190 -14783,5,111000060 -14784,5,112000060 -14785,5,108000110 -14786,5,110000110 -14787,5,118000070 -14788,1,201000010 -14789,1,292000010 -14790,1,299000040 -14791,1,299000070 -14792,1,299000110 -14793,1,299000120 -14794,1,299000140 -14795,2,202000010 -14796,2,290000010 -14797,2,299000010 -14798,2,299000150 -14799,2,299000190 -14800,2,299000200 -14801,2,299000210 -14802,3,298000050 -14803,3,298000060 -14804,3,299000060 -14805,3,299000170 -14806,3,290000120 -14807,3,291000050 -14808,3,292000020 -14809,4,299000670 -14810,4,299000680 -14811,4,204000010 -14812,4,209000040 -14813,4,202000070 -14814,4,209000070 -14815,4,203000110 -14816,4,290000110 -14817,4,206000110 -14818,5,297000100 -14819,5,291000020 -14820,5,297000130 -14821,5,297000140 -14822,5,203000010 -14823,5,206000030 -14824,5,203000050 -14825,5,202000090 -14826,5,204000080 -14827,5,202000150 -14828,5,204000100 -14829,3,170004 -14830,3,180004 -14831,4,140000 -14832,5,150010 -14833,5,150020 -14834,5,150030 -14835,5,150040 -14837,3,101000050 -14838,3,101000060 -14839,3,101000080 -14840,3,101000040 -14841,3,109000060 -14842,3,109000070 -14843,3,109000080 -14844,3,105000060 -14845,3,105000070 -14846,3,105000080 -14847,3,104000050 -14848,3,106000050 -14849,3,102000060 -14850,3,102000070 -14851,3,102000080 -14852,3,103000050 -14853,3,105000050 -14854,3,107000060 -14855,3,107000070 -14856,3,107000080 -14857,3,108000050 -14858,3,109000050 -14859,3,103000060 -14860,3,103000070 -14861,3,103000080 -14862,3,110000050 -14863,3,106000060 -14864,3,106000070 -14865,3,106000080 -14866,3,101000070 -14867,3,110000060 -14868,3,104000060 -14869,3,104000070 -14870,3,104000080 -14871,3,102000050 -14872,3,104000170 -14873,3,104000260 -14874,3,111000010 -14875,3,111000020 -14876,3,111000030 -14877,3,112000020 -14878,3,112000030 -14879,3,108000060 -14880,3,108000070 -14881,3,108000080 -14882,3,107000050 -14883,3,112000010 -14884,3,110000070 -14885,3,110000080 -14886,3,118000020 -14887,3,118000030 -14888,3,118000040 -14889,4,101000090 -14890,4,101000100 -14891,4,101000110 -14892,4,109000100 -14893,4,105000100 -14894,4,105000110 -14895,4,108000090 -14896,4,110000090 -14897,4,102000100 -14898,4,102000110 -14899,4,106000090 -14900,4,109000090 -14901,4,107000100 -14902,4,103000090 -14903,4,102000090 -14904,4,103000100 -14905,4,106000100 -14906,4,106000110 -14907,4,104000090 -14908,4,104000100 -14909,4,104000110 -14910,4,107000090 -14911,4,104000180 -14912,4,111000040 -14913,4,112000040 -14914,4,108000100 -14915,4,105000090 -14916,4,110000100 -14917,4,118000050 -14918,4,118000060 -14919,5,101000120 -14920,5,109000110 -14921,5,105000120 -14922,5,102000120 -14923,5,107000110 -14924,5,103000120 -14925,5,106000120 -14926,5,104000120 -14927,5,104000190 -14928,5,111000060 -14929,5,112000060 -14930,5,108000110 -14931,5,110000110 -14932,5,118000070 -14933,1,201000010 -14934,1,292000010 -14935,1,299000040 -14936,1,299000070 -14937,1,299000110 -14938,1,299000120 -14939,1,299000140 -14940,2,202000010 -14941,2,290000010 -14942,2,299000010 -14943,2,299000150 -14944,2,299000190 -14945,2,299000200 -14946,2,299000210 -14947,3,298000050 -14948,3,298000060 -14949,3,299000060 -14950,3,299000170 -14951,3,290000120 -14952,3,291000050 -14953,3,292000020 -14954,4,299000670 -14955,4,299000680 -14956,4,204000010 -14957,4,209000040 -14958,4,202000070 -14959,4,209000070 -14960,4,203000110 -14961,4,290000110 -14962,4,206000110 -14963,5,297000100 -14964,5,291000020 -14965,5,297000130 -14966,5,297000140 -14967,5,203000010 -14968,5,206000030 -14969,5,203000050 -14970,5,202000090 -14971,5,204000080 -14972,5,202000150 -14973,5,204000100 -14974,3,170004 -14975,3,180004 -14976,4,140000 -14977,5,150010 -14978,5,150020 -14979,5,150030 -14980,5,150040 -14982,3,101000050 -14983,3,101000060 -14984,3,101000080 -14985,3,101000040 -14986,3,109000060 -14987,3,109000070 -14988,3,109000080 -14989,3,105000060 -14990,3,105000070 -14991,3,105000080 -14992,3,104000050 -14993,3,106000050 -14994,3,102000060 -14995,3,102000070 -14996,3,102000080 -14997,3,103000050 -14998,3,105000050 -14999,3,107000060 -15000,3,107000070 -15001,3,107000080 -15002,3,108000050 -15003,3,109000050 -15004,3,103000060 -15005,3,103000070 -15006,3,103000080 -15007,3,110000050 -15008,3,106000060 -15009,3,106000070 -15010,3,106000080 -15011,3,101000070 -15012,3,110000060 -15013,3,104000060 -15014,3,104000070 -15015,3,104000080 -15016,3,102000050 -15017,3,104000170 -15018,3,104000260 -15019,3,111000010 -15020,3,111000020 -15021,3,111000030 -15022,3,112000020 -15023,3,112000030 -15024,3,108000060 -15025,3,108000070 -15026,3,108000080 -15027,3,107000050 -15028,3,112000010 -15029,3,110000070 -15030,3,110000080 -15031,3,118000020 -15032,3,118000030 -15033,3,118000040 -15034,4,101000090 -15035,4,101000100 -15036,4,101000110 -15037,4,109000100 -15038,4,105000100 -15039,4,105000110 -15040,4,108000090 -15041,4,110000090 -15042,4,102000100 -15043,4,102000110 -15044,4,106000090 -15045,4,109000090 -15046,4,107000100 -15047,4,103000090 -15048,4,102000090 -15049,4,103000100 -15050,4,106000100 -15051,4,106000110 -15052,4,104000090 -15053,4,104000100 -15054,4,104000110 -15055,4,107000090 -15056,4,104000180 -15057,4,111000040 -15058,4,112000040 -15059,4,108000100 -15060,4,105000090 -15061,4,110000100 -15062,4,118000050 -15063,4,118000060 -15064,5,101000120 -15065,5,109000110 -15066,5,105000120 -15067,5,102000120 -15068,5,107000110 -15069,5,103000120 -15070,5,106000120 -15071,5,104000120 -15072,5,104000190 -15073,5,111000060 -15074,5,112000060 -15075,5,108000110 -15076,5,110000110 -15077,5,118000070 -15078,1,201000010 -15079,1,292000010 -15080,1,299000040 -15081,1,299000070 -15082,1,299000110 -15083,1,299000120 -15084,1,299000140 -15085,2,202000010 -15086,2,290000010 -15087,2,299000010 -15088,2,299000150 -15089,2,299000190 -15090,2,299000200 -15091,2,299000210 -15092,3,298000050 -15093,3,298000060 -15094,3,299000060 -15095,3,299000170 -15096,3,290000120 -15097,3,291000050 -15098,3,292000020 -15099,4,299000670 -15100,4,299000680 -15101,4,204000010 -15102,4,209000040 -15103,4,202000070 -15104,4,209000070 -15105,4,203000110 -15106,4,290000110 -15107,4,206000110 -15108,5,297000100 -15109,5,291000020 -15110,5,297000130 -15111,5,297000140 -15112,5,203000010 -15113,5,206000030 -15114,5,203000050 -15115,5,202000090 -15116,5,204000080 -15117,5,202000150 -15118,5,204000100 -15119,3,170004 -15120,3,180004 -15121,4,140000 -15122,5,150010 -15123,5,150020 -15124,5,150030 -15125,5,150040 -15126,5,190000 -15127,5,200000 -15128,5,210000 -15130,3,111000010 -15131,3,111000020 -15132,3,111000030 -15133,3,112000020 -15134,3,112000030 -15135,3,108000060 -15136,3,108000070 -15137,3,108000080 -15138,3,107000050 -15139,3,112000010 -15140,3,110000070 -15141,3,110000080 -15142,4,111000040 -15143,4,112000040 -15144,4,108000100 -15145,4,105000090 -15146,4,110000100 -15147,5,111000060 -15148,5,112000060 -15149,5,108000110 -15150,5,110000110 -15151,1,108000000 -15152,2,108000001 -15153,3,108000002 -15154,4,108000003 -15155,5,108000004 -15156,1,107000000 -15157,2,107000001 -15158,3,107000002 -15159,4,107000003 -15160,5,107000004 -15161,1,120000000 -15162,2,120000001 -15163,3,120000002 -15164,4,120000003 -15165,5,120000004 -15166,3,170004 -15167,4,170005 -15168,3,180004 -15169,4,180005 -15170,4,140000 -15171,4,150010 -15172,4,150020 -15173,4,150030 -15174,4,150040 -15176,3,101000050 -15177,3,101000060 -15178,3,101000080 -15179,3,101000040 -15180,3,109000060 -15181,3,109000070 -15182,3,109000080 -15183,3,105000060 -15184,3,105000070 -15185,3,105000080 -15186,3,104000050 -15187,3,106000050 -15188,4,101000090 -15189,4,101000100 -15190,4,101000110 -15191,4,109000100 -15192,4,105000100 -15193,4,105000110 -15194,4,108000090 -15195,4,110000090 -15196,5,101000120 -15197,5,109000110 -15198,5,105000120 -15199,1,101000000 -15200,2,101000001 -15201,3,101000002 -15202,4,101000008 -15203,5,101000004 -15204,1,109000000 -15205,2,109000001 -15206,3,109000002 -15207,4,109000003 -15208,5,109000004 -15209,4,110024 -15210,4,110034 -15211,4,110044 -15212,4,110054 -15213,3,110060 -15214,3,110070 -15215,3,110080 -15216,3,110090 -15217,3,110100 -15218,3,110110 -15219,3,110120 -15220,3,110130 -15221,3,110140 -15222,3,110150 -15223,3,110160 -15224,3,110170 -15225,3,110180 -15226,3,110190 -15227,3,110200 -15228,3,110210 -15229,3,110220 -15230,3,110230 -15231,3,110240 -15232,3,110250 -15233,3,110260 -15234,3,110270 -15235,3,110620 -15236,3,110670 -15237,4,140000 -15238,4,150010 -15239,4,150020 -15240,4,150030 -15241,4,150040 -15243,3,102000060 -15244,3,102000070 -15245,3,102000080 -15246,3,103000050 -15247,3,105000050 -15248,3,107000060 -15249,3,107000070 -15250,3,107000080 -15251,3,108000050 -15252,3,109000050 -15253,3,103000060 -15254,3,103000070 -15255,3,103000080 -15256,3,110000050 -15257,4,102000100 -15258,4,102000110 -15259,4,102000350 -15260,4,106000090 -15261,4,109000090 -15262,4,107000100 -15263,4,103000090 -15264,4,102000090 -15265,4,103000100 -15266,5,102000120 -15267,5,107000110 -15268,5,103000120 -15269,1,102000000 -15270,2,102000001 -15271,3,102000002 -15272,4,102000003 -15273,5,102000004 -15274,1,105000000 -15275,2,105000001 -15276,3,105000002 -15277,4,105000003 -15278,5,105000004 -15279,1,112000000 -15280,2,112000001 -15281,3,112000002 -15282,4,112000003 -15283,5,112000004 -15284,4,120024 -15285,4,120034 -15286,4,120044 -15287,4,120054 -15288,3,120241 -15289,3,120251 -15290,3,120261 -15291,3,120271 -15292,3,120300 -15293,3,120310 -15294,3,120320 -15295,3,120330 -15296,3,120340 -15297,3,120350 -15298,3,120360 -15299,3,120370 -15300,3,120380 -15301,3,120390 -15302,3,120400 -15303,3,120410 -15304,3,120420 -15305,3,120430 -15306,3,120450 -15307,3,120460 -15308,3,120550 -15309,3,120560 -15310,3,120570 -15311,3,120990 -15312,3,121000 -15313,3,121010 -15314,3,121020 -15315,3,121100 -15316,4,140000 -15317,4,150010 -15318,4,150020 -15319,4,150030 -15320,4,150040 -15322,3,118000020 -15323,3,118000030 -15324,3,118000040 -15325,3,106000060 -15326,3,106000070 -15327,3,106000080 -15328,3,101000070 -15329,3,110000060 -15330,3,104000060 -15331,3,104000070 -15332,3,104000080 -15333,3,102000050 -15334,3,104000170 -15335,3,104000260 -15336,4,118000050 -15337,4,118000060 -15338,4,106000100 -15339,4,106000110 -15340,4,104000090 -15341,4,104000100 -15342,4,104000110 -15343,4,104000270 -15344,4,107000090 -15345,4,104000180 -15346,5,118000070 -15347,5,106000120 -15348,5,104000120 -15349,5,104000190 -15350,1,103000000 -15351,2,103000001 -15352,3,103000002 -15353,4,103000003 -15354,5,103000004 -15355,1,111000000 -15356,2,111000001 -15357,3,111000002 -15358,4,111000003 -15359,5,111000004 -15360,1,115000000 -15361,2,115000001 -15362,3,115000002 -15363,4,115000003 -15364,5,115000004 -15365,4,130024 -15366,4,130034 -15367,4,130044 -15368,4,130054 -15369,3,130060 -15370,3,130070 -15371,3,130080 -15372,3,130090 -15373,3,130100 -15374,3,130110 -15375,3,130120 -15376,3,130130 -15377,3,130140 -15378,3,130150 -15379,3,130160 -15380,3,130170 -15381,3,130180 -15382,3,130190 -15383,3,130200 -15384,3,130420 -15385,3,130510 -15386,3,130520 -15387,3,130531 -15388,3,130540 -15389,3,130660 -15390,3,130700 -15391,3,130790 -15392,3,130800 -15393,3,131130 -15394,4,140000 -15395,4,150010 -15396,4,150020 -15397,4,150030 -15398,4,150040 -15400,1,101000010 -15401,1,102000010 -15402,1,103000010 -15403,1,104000010 -15404,1,105000010 -15405,1,106000010 -15406,1,107000010 -15407,1,108000010 -15408,1,109000010 -15409,1,110000010 -15410,2,101000020 -15411,2,101000030 -15412,2,102000020 -15413,2,102000030 -15414,2,102000040 -15415,2,103000020 -15416,2,103000030 -15417,2,103000040 -15418,2,104000020 -15419,2,104000030 -15420,2,104000040 -15421,2,105000020 -15422,2,105000030 -15423,2,105000040 -15424,2,106000020 -15425,2,106000030 -15426,2,106000040 -15427,2,107000020 -15428,2,107000030 -15429,2,107000040 -15430,2,108000020 -15431,2,108000030 -15432,2,108000040 -15433,2,109000020 -15434,2,109000030 -15435,2,109000040 -15436,2,110000020 -15437,2,110000030 -15438,2,110000040 -15439,2,118000010 -15440,3,101000050 -15441,3,101000060 -15442,3,101000080 -15443,3,101000040 -15444,3,109000060 -15445,3,109000070 -15446,3,109000080 -15447,3,105000060 -15448,3,105000070 -15449,3,105000080 -15450,3,104000050 -15451,3,106000050 -15452,3,102000060 -15453,3,102000070 -15454,3,102000080 -15455,3,103000050 -15456,3,105000050 -15457,3,107000060 -15458,3,107000070 -15459,3,107000080 -15460,3,108000050 -15461,3,109000050 -15462,3,103000060 -15463,3,103000070 -15464,3,103000080 -15465,3,110000050 -15466,3,106000060 -15467,3,106000070 -15468,3,106000080 -15469,3,101000070 -15470,3,110000060 -15471,3,104000060 -15472,3,104000070 -15473,3,104000080 -15474,3,102000050 -15475,3,104000170 -15476,3,104000260 -15477,3,111000010 -15478,3,111000020 -15479,3,111000030 -15480,3,112000020 -15481,3,112000030 -15482,3,108000060 -15483,3,108000070 -15484,3,108000080 -15485,3,107000050 -15486,3,112000010 -15487,3,110000070 -15488,3,110000080 -15489,3,118000020 -15490,3,118000030 -15491,3,118000040 -15492,4,101000090 -15493,4,101000100 -15494,4,101000110 -15495,4,109000100 -15496,4,105000100 -15497,4,105000110 -15498,4,108000090 -15499,4,110000090 -15500,4,102000100 -15501,4,102000110 -15502,4,106000090 -15503,4,109000090 -15504,4,107000100 -15505,4,103000090 -15506,4,102000090 -15507,4,103000100 -15508,4,106000100 -15509,4,106000110 -15510,4,104000090 -15511,4,104000100 -15512,4,104000110 -15513,4,107000090 -15514,4,104000180 -15515,4,111000040 -15516,4,112000040 -15517,4,108000100 -15518,4,105000090 -15519,4,110000100 -15520,4,118000050 -15521,4,118000060 -15522,5,101000120 -15523,5,109000110 -15524,5,105000120 -15525,5,102000120 -15526,5,107000110 -15527,5,103000120 -15528,5,106000120 -15529,5,104000120 -15530,5,104000190 -15531,5,111000060 -15532,5,112000060 -15533,5,108000110 -15534,5,110000110 -15535,5,118000070 -15536,1,201000010 -15537,1,292000010 -15538,1,299000040 -15539,1,299000070 -15540,1,299000110 -15541,1,299000120 -15542,1,299000140 -15543,2,202000010 -15544,2,290000010 -15545,2,299000010 -15546,2,299000150 -15547,2,299000190 -15548,2,299000200 -15549,2,299000210 -15550,3,298000050 -15551,3,298000060 -15552,3,299000060 -15553,3,299000170 -15554,3,290000120 -15555,3,291000050 -15556,3,292000020 -15557,4,299000670 -15558,4,299000680 -15559,4,204000010 -15560,4,209000040 -15561,4,202000070 -15562,4,209000070 -15563,4,203000110 -15564,4,290000110 -15565,4,206000110 -15566,4,209000160 -15567,5,297000100 -15568,5,291000020 -15569,5,297000130 -15570,5,297000140 -15571,5,203000010 -15572,5,206000030 -15573,5,203000050 -15574,5,202000090 -15575,5,204000080 -15576,5,202000150 -15577,5,204000100 -15578,5,206000170 -15579,1,170002 -15580,1,180002 -15581,2,170003 -15582,2,180003 -15583,3,170004 -15584,3,180004 -15585,4,140000 -15586,5,150010 -15587,5,150020 -15588,5,150030 -15589,5,150040 -15591,1,101000010 -15592,1,102000010 -15593,1,103000010 -15594,1,104000010 -15595,1,105000010 -15596,1,106000010 -15597,1,107000010 -15598,1,108000010 -15599,1,109000010 -15600,1,110000010 -15601,2,101000020 -15602,2,101000030 -15603,2,102000020 -15604,2,102000030 -15605,2,102000040 -15606,2,103000020 -15607,2,103000030 -15608,2,103000040 -15609,2,104000020 -15610,2,104000030 -15611,2,104000040 -15612,2,105000020 -15613,2,105000030 -15614,2,105000040 -15615,2,106000020 -15616,2,106000030 -15617,2,106000040 -15618,2,107000020 -15619,2,107000030 -15620,2,107000040 -15621,2,108000020 -15622,2,108000030 -15623,2,108000040 -15624,2,109000020 -15625,2,109000030 -15626,2,109000040 -15627,2,110000020 -15628,2,110000030 -15629,2,110000040 -15630,2,118000010 -15631,3,101000050 -15632,3,101000060 -15633,3,101000080 -15634,3,101000040 -15635,3,109000060 -15636,3,109000070 -15637,3,109000080 -15638,3,105000060 -15639,3,105000070 -15640,3,105000080 -15641,3,104000050 -15642,3,106000050 -15643,3,102000060 -15644,3,102000070 -15645,3,102000080 -15646,3,103000050 -15647,3,105000050 -15648,3,107000060 -15649,3,107000070 -15650,3,107000080 -15651,3,108000050 -15652,3,109000050 -15653,3,103000060 -15654,3,103000070 -15655,3,103000080 -15656,3,110000050 -15657,3,106000060 -15658,3,106000070 -15659,3,106000080 -15660,3,101000070 -15661,3,110000060 -15662,3,104000060 -15663,3,104000070 -15664,3,104000080 -15665,3,102000050 -15666,3,104000170 -15667,3,104000260 -15668,3,111000010 -15669,3,111000020 -15670,3,111000030 -15671,3,112000020 -15672,3,112000030 -15673,3,108000060 -15674,3,108000070 -15675,3,108000080 -15676,3,107000050 -15677,3,112000010 -15678,3,110000070 -15679,3,110000080 -15680,3,118000020 -15681,3,118000030 -15682,3,118000040 -15683,4,101000090 -15684,4,101000100 -15685,4,101000110 -15686,4,109000100 -15687,4,105000100 -15688,4,105000110 -15689,4,108000090 -15690,4,110000090 -15691,4,102000100 -15692,4,102000110 -15693,4,106000090 -15694,4,109000090 -15695,4,107000100 -15696,4,103000090 -15697,4,102000090 -15698,4,103000100 -15699,4,106000100 -15700,4,106000110 -15701,4,104000090 -15702,4,104000100 -15703,4,104000110 -15704,4,107000090 -15705,4,104000180 -15706,4,111000040 -15707,4,112000040 -15708,4,108000100 -15709,4,105000090 -15710,4,110000100 -15711,4,118000050 -15712,4,118000060 -15713,5,101000120 -15714,5,109000110 -15715,5,105000120 -15716,5,102000120 -15717,5,107000110 -15718,5,103000120 -15719,5,106000120 -15720,5,104000120 -15721,5,104000190 -15722,5,111000060 -15723,5,112000060 -15724,5,108000110 -15725,5,110000110 -15726,5,118000070 -15727,1,201000010 -15728,1,292000010 -15729,1,299000040 -15730,1,299000070 -15731,1,299000110 -15732,1,299000120 -15733,1,299000140 -15734,2,202000010 -15735,2,290000010 -15736,2,299000010 -15737,2,299000150 -15738,2,299000190 -15739,2,299000200 -15740,2,299000210 -15741,3,298000050 -15742,3,298000060 -15743,3,299000060 -15744,3,299000170 -15745,3,290000120 -15746,3,291000050 -15747,3,292000020 -15748,4,299000670 -15749,4,299000680 -15750,4,204000010 -15751,4,209000040 -15752,4,202000070 -15753,4,209000070 -15754,4,203000110 -15755,4,290000110 -15756,4,206000110 -15757,4,209000160 -15758,5,297000100 -15759,5,291000020 -15760,5,297000130 -15761,5,297000140 -15762,5,203000010 -15763,5,206000030 -15764,5,203000050 -15765,5,202000090 -15766,5,204000080 -15767,5,202000150 -15768,5,204000100 -15769,5,206000170 -15770,2,170003 -15771,2,180003 -15772,3,170004 -15773,3,180004 -15774,4,140000 -15775,5,150010 -15776,5,150020 -15777,5,150030 -15778,5,150040 -15780,3,101000050 -15781,3,101000060 -15782,3,101000080 -15783,3,101000040 -15784,3,109000060 -15785,3,109000070 -15786,3,109000080 -15787,3,105000060 -15788,3,105000070 -15789,3,105000080 -15790,3,104000050 -15791,3,106000050 -15792,3,102000060 -15793,3,102000070 -15794,3,102000080 -15795,3,103000050 -15796,3,105000050 -15797,3,107000060 -15798,3,107000070 -15799,3,107000080 -15800,3,108000050 -15801,3,109000050 -15802,3,103000060 -15803,3,103000070 -15804,3,103000080 -15805,3,110000050 -15806,3,106000060 -15807,3,106000070 -15808,3,106000080 -15809,3,101000070 -15810,3,110000060 -15811,3,104000060 -15812,3,104000070 -15813,3,104000080 -15814,3,102000050 -15815,3,104000170 -15816,3,104000260 -15817,3,111000010 -15818,3,111000020 -15819,3,111000030 -15820,3,112000020 -15821,3,112000030 -15822,3,108000060 -15823,3,108000070 -15824,3,108000080 -15825,3,107000050 -15826,3,112000010 -15827,3,110000070 -15828,3,110000080 -15829,3,118000020 -15830,3,118000030 -15831,3,118000040 -15832,4,101000090 -15833,4,101000100 -15834,4,101000110 -15835,4,109000100 -15836,4,105000100 -15837,4,105000110 -15838,4,108000090 -15839,4,110000090 -15840,4,102000100 -15841,4,102000110 -15842,4,106000090 -15843,4,109000090 -15844,4,107000100 -15845,4,103000090 -15846,4,102000090 -15847,4,103000100 -15848,4,106000100 -15849,4,106000110 -15850,4,104000090 -15851,4,104000100 -15852,4,104000110 -15853,4,107000090 -15854,4,104000180 -15855,4,111000040 -15856,4,112000040 -15857,4,108000100 -15858,4,105000090 -15859,4,110000100 -15860,4,118000050 -15861,4,118000060 -15862,5,101000120 -15863,5,109000110 -15864,5,105000120 -15865,5,102000120 -15866,5,107000110 -15867,5,103000120 -15868,5,106000120 -15869,5,104000120 -15870,5,104000190 -15871,5,111000060 -15872,5,112000060 -15873,5,108000110 -15874,5,110000110 -15875,5,118000070 -15876,1,201000010 -15877,1,292000010 -15878,1,299000040 -15879,1,299000070 -15880,1,299000110 -15881,1,299000120 -15882,1,299000140 -15883,2,202000010 -15884,2,290000010 -15885,2,299000010 -15886,2,299000150 -15887,2,299000190 -15888,2,299000200 -15889,2,299000210 -15890,3,298000050 -15891,3,298000060 -15892,3,299000060 -15893,3,299000170 -15894,3,290000120 -15895,3,291000050 -15896,3,292000020 -15897,4,299000670 -15898,4,299000680 -15899,4,204000010 -15900,4,209000040 -15901,4,202000070 -15902,4,209000070 -15903,4,203000110 -15904,4,290000110 -15905,4,206000110 -15906,4,209000160 -15907,5,297000100 -15908,5,291000020 -15909,5,297000130 -15910,5,297000140 -15911,5,203000010 -15912,5,206000030 -15913,5,203000050 -15914,5,202000090 -15915,5,204000080 -15916,5,202000150 -15917,5,204000100 -15918,5,206000170 -15919,3,170004 -15920,3,180004 -15921,4,140000 -15922,5,150010 -15923,5,150020 -15924,5,150030 -15925,5,150040 -15927,3,101000050 -15928,3,101000060 -15929,3,101000080 -15930,3,101000040 -15931,3,109000060 -15932,3,109000070 -15933,3,109000080 -15934,3,105000060 -15935,3,105000070 -15936,3,105000080 -15937,3,104000050 -15938,3,106000050 -15939,3,102000060 -15940,3,102000070 -15941,3,102000080 -15942,3,103000050 -15943,3,105000050 -15944,3,107000060 -15945,3,107000070 -15946,3,107000080 -15947,3,108000050 -15948,3,109000050 -15949,3,103000060 -15950,3,103000070 -15951,3,103000080 -15952,3,110000050 -15953,3,106000060 -15954,3,106000070 -15955,3,106000080 -15956,3,101000070 -15957,3,110000060 -15958,3,104000060 -15959,3,104000070 -15960,3,104000080 -15961,3,102000050 -15962,3,104000170 -15963,3,104000260 -15964,3,111000010 -15965,3,111000020 -15966,3,111000030 -15967,3,112000020 -15968,3,112000030 -15969,3,108000060 -15970,3,108000070 -15971,3,108000080 -15972,3,107000050 -15973,3,112000010 -15974,3,110000070 -15975,3,110000080 -15976,3,118000020 -15977,3,118000030 -15978,3,118000040 -15979,4,101000090 -15980,4,101000100 -15981,4,101000110 -15982,4,109000100 -15983,4,105000100 -15984,4,105000110 -15985,4,108000090 -15986,4,110000090 -15987,4,102000100 -15988,4,102000110 -15989,4,106000090 -15990,4,109000090 -15991,4,107000100 -15992,4,103000090 -15993,4,102000090 -15994,4,103000100 -15995,4,106000100 -15996,4,106000110 -15997,4,104000090 -15998,4,104000100 -15999,4,104000110 -16000,4,107000090 -16001,4,104000180 -16002,4,111000040 -16003,4,112000040 -16004,4,108000100 -16005,4,105000090 -16006,4,110000100 -16007,4,118000050 -16008,4,118000060 -16009,5,101000120 -16010,5,109000110 -16011,5,105000120 -16012,5,102000120 -16013,5,107000110 -16014,5,103000120 -16015,5,106000120 -16016,5,104000120 -16017,5,104000190 -16018,5,111000060 -16019,5,112000060 -16020,5,108000110 -16021,5,110000110 -16022,5,118000070 -16023,1,201000010 -16024,1,292000010 -16025,1,299000040 -16026,1,299000070 -16027,1,299000110 -16028,1,299000120 -16029,1,299000140 -16030,2,202000010 -16031,2,290000010 -16032,2,299000010 -16033,2,299000150 -16034,2,299000190 -16035,2,299000200 -16036,2,299000210 -16037,3,298000050 -16038,3,298000060 -16039,3,299000060 -16040,3,299000170 -16041,3,290000120 -16042,3,291000050 -16043,3,292000020 -16044,4,299000670 -16045,4,299000680 -16046,4,204000010 -16047,4,209000040 -16048,4,202000070 -16049,4,209000070 -16050,4,203000110 -16051,4,290000110 -16052,4,206000110 -16053,4,209000160 -16054,5,297000100 -16055,5,291000020 -16056,5,297000130 -16057,5,297000140 -16058,5,203000010 -16059,5,206000030 -16060,5,203000050 -16061,5,202000090 -16062,5,204000080 -16063,5,202000150 -16064,5,204000100 -16065,5,206000170 -16066,3,170004 -16067,3,180004 -16068,4,140000 -16069,5,150010 -16070,5,150020 -16071,5,150030 -16072,5,150040 -16074,3,101000050 -16075,3,101000060 -16076,3,101000080 -16077,3,101000040 -16078,3,109000060 -16079,3,109000070 -16080,3,109000080 -16081,3,105000060 -16082,3,105000070 -16083,3,105000080 -16084,3,104000050 -16085,3,106000050 -16086,3,102000060 -16087,3,102000070 -16088,3,102000080 -16089,3,103000050 -16090,3,105000050 -16091,3,107000060 -16092,3,107000070 -16093,3,107000080 -16094,3,108000050 -16095,3,109000050 -16096,3,103000060 -16097,3,103000070 -16098,3,103000080 -16099,3,110000050 -16100,3,106000060 -16101,3,106000070 -16102,3,106000080 -16103,3,101000070 -16104,3,110000060 -16105,3,104000060 -16106,3,104000070 -16107,3,104000080 -16108,3,102000050 -16109,3,104000170 -16110,3,104000260 -16111,3,111000010 -16112,3,111000020 -16113,3,111000030 -16114,3,112000020 -16115,3,112000030 -16116,3,108000060 -16117,3,108000070 -16118,3,108000080 -16119,3,107000050 -16120,3,112000010 -16121,3,110000070 -16122,3,110000080 -16123,3,118000020 -16124,3,118000030 -16125,3,118000040 -16126,4,101000090 -16127,4,101000100 -16128,4,101000110 -16129,4,109000100 -16130,4,105000100 -16131,4,105000110 -16132,4,108000090 -16133,4,110000090 -16134,4,102000100 -16135,4,102000110 -16136,4,106000090 -16137,4,109000090 -16138,4,107000100 -16139,4,103000090 -16140,4,102000090 -16141,4,103000100 -16142,4,106000100 -16143,4,106000110 -16144,4,104000090 -16145,4,104000100 -16146,4,104000110 -16147,4,107000090 -16148,4,104000180 -16149,4,111000040 -16150,4,112000040 -16151,4,108000100 -16152,4,105000090 -16153,4,110000100 -16154,4,118000050 -16155,4,118000060 -16156,5,101000120 -16157,5,109000110 -16158,5,105000120 -16159,5,102000120 -16160,5,107000110 -16161,5,103000120 -16162,5,106000120 -16163,5,104000120 -16164,5,104000190 -16165,5,111000060 -16166,5,112000060 -16167,5,108000110 -16168,5,110000110 -16169,5,118000070 -16170,1,201000010 -16171,1,292000010 -16172,1,299000040 -16173,1,299000070 -16174,1,299000110 -16175,1,299000120 -16176,1,299000140 -16177,2,202000010 -16178,2,290000010 -16179,2,299000010 -16180,2,299000150 -16181,2,299000190 -16182,2,299000200 -16183,2,299000210 -16184,3,298000050 -16185,3,298000060 -16186,3,299000060 -16187,3,299000170 -16188,3,290000120 -16189,3,291000050 -16190,3,292000020 -16191,4,299000670 -16192,4,299000680 -16193,4,204000010 -16194,4,209000040 -16195,4,202000070 -16196,4,209000070 -16197,4,203000110 -16198,4,290000110 -16199,4,206000110 -16200,4,209000160 -16201,5,297000100 -16202,5,291000020 -16203,5,297000130 -16204,5,297000140 -16205,5,203000010 -16206,5,206000030 -16207,5,203000050 -16208,5,202000090 -16209,5,204000080 -16210,5,202000150 -16211,5,204000100 -16212,5,206000170 -16213,3,170004 -16214,3,180004 -16215,4,140000 -16216,5,150010 -16217,5,150020 -16218,5,150030 -16219,5,150040 -16220,5,190000 -16221,5,200000 -16222,5,210000 -16224,1,101000010 -16225,1,102000010 -16226,1,103000010 -16227,1,104000010 -16228,1,105000010 -16229,1,106000010 -16230,1,107000010 -16231,1,108000010 -16232,1,109000010 -16233,1,110000010 -16234,2,101000020 -16235,2,101000030 -16236,2,102000020 -16237,2,102000030 -16238,2,102000040 -16239,2,103000020 -16240,2,103000030 -16241,2,103000040 -16242,2,104000020 -16243,2,104000030 -16244,2,104000040 -16245,2,105000020 -16246,2,105000030 -16247,2,105000040 -16248,2,106000020 -16249,2,106000030 -16250,2,106000040 -16251,2,107000020 -16252,2,107000030 -16253,2,107000040 -16254,2,108000020 -16255,2,108000030 -16256,2,108000040 -16257,2,109000020 -16258,2,109000030 -16259,2,109000040 -16260,2,110000020 -16261,2,110000030 -16262,2,110000040 -16263,2,118000010 -16264,3,101000050 -16265,3,101000060 -16266,3,101000080 -16267,3,101000040 -16268,3,109000060 -16269,3,109000070 -16270,3,109000080 -16271,3,105000060 -16272,3,105000070 -16273,3,105000080 -16274,3,104000050 -16275,3,106000050 -16276,3,102000060 -16277,3,102000070 -16278,3,102000080 -16279,3,103000050 -16280,3,105000050 -16281,3,107000060 -16282,3,107000070 -16283,3,107000080 -16284,3,108000050 -16285,3,109000050 -16286,3,103000060 -16287,3,103000070 -16288,3,103000080 -16289,3,110000050 -16290,3,106000060 -16291,3,106000070 -16292,3,106000080 -16293,3,101000070 -16294,3,110000060 -16295,3,104000060 -16296,3,104000070 -16297,3,104000080 -16298,3,102000050 -16299,3,104000170 -16300,3,104000260 -16301,3,111000010 -16302,3,111000020 -16303,3,111000030 -16304,3,112000020 -16305,3,112000030 -16306,3,108000060 -16307,3,108000070 -16308,3,108000080 -16309,3,107000050 -16310,3,112000010 -16311,3,110000070 -16312,3,110000080 -16313,3,118000020 -16314,3,118000030 -16315,3,118000040 -16316,4,101000090 -16317,4,101000100 -16318,4,101000110 -16319,4,109000100 -16320,4,105000100 -16321,4,105000110 -16322,4,108000090 -16323,4,110000090 -16324,4,102000100 -16325,4,102000110 -16326,4,106000090 -16327,4,109000090 -16328,4,107000100 -16329,4,103000090 -16330,4,102000090 -16331,4,103000100 -16332,4,106000100 -16333,4,106000110 -16334,4,104000090 -16335,4,104000100 -16336,4,104000110 -16337,4,107000090 -16338,4,104000180 -16339,4,111000040 -16340,4,112000040 -16341,4,108000100 -16342,4,105000090 -16343,4,110000100 -16344,4,118000050 -16345,4,118000060 -16346,5,101000120 -16347,5,109000110 -16348,5,105000120 -16349,5,102000120 -16350,5,107000110 -16351,5,103000120 -16352,5,106000120 -16353,5,104000120 -16354,5,104000190 -16355,5,111000060 -16356,5,112000060 -16357,5,108000110 -16358,5,110000110 -16359,5,118000070 -16360,1,201000010 -16361,1,292000010 -16362,1,299000040 -16363,1,299000070 -16364,1,299000110 -16365,1,299000120 -16366,1,299000140 -16367,2,202000010 -16368,2,290000010 -16369,2,299000010 -16370,2,299000150 -16371,2,299000190 -16372,2,299000200 -16373,2,299000210 -16374,3,298000050 -16375,3,298000060 -16376,3,299000060 -16377,3,299000170 -16378,3,290000120 -16379,3,291000050 -16380,3,292000020 -16381,4,299000670 -16382,4,299000680 -16383,4,204000010 -16384,4,209000040 -16385,4,202000070 -16386,4,209000070 -16387,4,203000110 -16388,4,290000110 -16389,4,206000110 -16390,4,209000160 -16391,4,209000190 -16392,5,297000100 -16393,5,291000020 -16394,5,297000130 -16395,5,297000140 -16396,5,203000010 -16397,5,206000030 -16398,5,203000050 -16399,5,202000090 -16400,5,204000080 -16401,5,202000150 -16402,5,204000100 -16403,5,206000170 -16404,5,204000200 -16405,1,170002 -16406,1,180002 -16407,2,170003 -16408,2,180003 -16409,3,170004 -16410,3,180004 -16411,4,140000 -16412,5,150010 -16413,5,150020 -16414,5,150030 -16415,5,150040 -16417,1,101000010 -16418,1,102000010 -16419,1,103000010 -16420,1,104000010 -16421,1,105000010 -16422,1,106000010 -16423,1,107000010 -16424,1,108000010 -16425,1,109000010 -16426,1,110000010 -16427,2,101000020 -16428,2,101000030 -16429,2,102000020 -16430,2,102000030 -16431,2,102000040 -16432,2,103000020 -16433,2,103000030 -16434,2,103000040 -16435,2,104000020 -16436,2,104000030 -16437,2,104000040 -16438,2,105000020 -16439,2,105000030 -16440,2,105000040 -16441,2,106000020 -16442,2,106000030 -16443,2,106000040 -16444,2,107000020 -16445,2,107000030 -16446,2,107000040 -16447,2,108000020 -16448,2,108000030 -16449,2,108000040 -16450,2,109000020 -16451,2,109000030 -16452,2,109000040 -16453,2,110000020 -16454,2,110000030 -16455,2,110000040 -16456,2,118000010 -16457,3,101000050 -16458,3,101000060 -16459,3,101000080 -16460,3,101000040 -16461,3,109000060 -16462,3,109000070 -16463,3,109000080 -16464,3,105000060 -16465,3,105000070 -16466,3,105000080 -16467,3,104000050 -16468,3,106000050 -16469,3,102000060 -16470,3,102000070 -16471,3,102000080 -16472,3,103000050 -16473,3,105000050 -16474,3,107000060 -16475,3,107000070 -16476,3,107000080 -16477,3,108000050 -16478,3,109000050 -16479,3,103000060 -16480,3,103000070 -16481,3,103000080 -16482,3,110000050 -16483,3,106000060 -16484,3,106000070 -16485,3,106000080 -16486,3,101000070 -16487,3,110000060 -16488,3,104000060 -16489,3,104000070 -16490,3,104000080 -16491,3,102000050 -16492,3,104000170 -16493,3,104000260 -16494,3,111000010 -16495,3,111000020 -16496,3,111000030 -16497,3,112000020 -16498,3,112000030 -16499,3,108000060 -16500,3,108000070 -16501,3,108000080 -16502,3,107000050 -16503,3,112000010 -16504,3,110000070 -16505,3,110000080 -16506,3,118000020 -16507,3,118000030 -16508,3,118000040 -16509,4,101000090 -16510,4,101000100 -16511,4,101000110 -16512,4,109000100 -16513,4,105000100 -16514,4,105000110 -16515,4,108000090 -16516,4,110000090 -16517,4,102000100 -16518,4,102000110 -16519,4,106000090 -16520,4,109000090 -16521,4,107000100 -16522,4,103000090 -16523,4,102000090 -16524,4,103000100 -16525,4,106000100 -16526,4,106000110 -16527,4,104000090 -16528,4,104000100 -16529,4,104000110 -16530,4,107000090 -16531,4,104000180 -16532,4,111000040 -16533,4,112000040 -16534,4,108000100 -16535,4,105000090 -16536,4,110000100 -16537,4,118000050 -16538,4,118000060 -16539,5,101000120 -16540,5,109000110 -16541,5,105000120 -16542,5,102000120 -16543,5,107000110 -16544,5,103000120 -16545,5,106000120 -16546,5,104000120 -16547,5,104000190 -16548,5,111000060 -16549,5,112000060 -16550,5,108000110 -16551,5,110000110 -16552,5,118000070 -16553,1,201000010 -16554,1,292000010 -16555,1,299000040 -16556,1,299000070 -16557,1,299000110 -16558,1,299000120 -16559,1,299000140 -16560,2,202000010 -16561,2,290000010 -16562,2,299000010 -16563,2,299000150 -16564,2,299000190 -16565,2,299000200 -16566,2,299000210 -16567,3,298000050 -16568,3,298000060 -16569,3,299000060 -16570,3,299000170 -16571,3,290000120 -16572,3,291000050 -16573,3,292000020 -16574,4,299000670 -16575,4,299000680 -16576,4,204000010 -16577,4,209000040 -16578,4,202000070 -16579,4,209000070 -16580,4,203000110 -16581,4,290000110 -16582,4,206000110 -16583,4,209000160 -16584,4,209000190 -16585,5,297000100 -16586,5,291000020 -16587,5,297000130 -16588,5,297000140 -16589,5,203000010 -16590,5,206000030 -16591,5,203000050 -16592,5,202000090 -16593,5,204000080 -16594,5,202000150 -16595,5,204000100 -16596,5,206000170 -16597,5,204000200 -16598,2,170003 -16599,2,180003 -16600,3,170004 -16601,3,180004 -16602,4,140000 -16603,5,150010 -16604,5,150020 -16605,5,150030 -16606,5,150040 -16608,3,101000050 -16609,3,101000060 -16610,3,101000080 -16611,3,101000040 -16612,3,109000060 -16613,3,109000070 -16614,3,109000080 -16615,3,105000060 -16616,3,105000070 -16617,3,105000080 -16618,3,104000050 -16619,3,106000050 -16620,3,102000060 -16621,3,102000070 -16622,3,102000080 -16623,3,103000050 -16624,3,105000050 -16625,3,107000060 -16626,3,107000070 -16627,3,107000080 -16628,3,108000050 -16629,3,109000050 -16630,3,103000060 -16631,3,103000070 -16632,3,103000080 -16633,3,110000050 -16634,3,106000060 -16635,3,106000070 -16636,3,106000080 -16637,3,101000070 -16638,3,110000060 -16639,3,104000060 -16640,3,104000070 -16641,3,104000080 -16642,3,102000050 -16643,3,104000170 -16644,3,104000260 -16645,3,111000010 -16646,3,111000020 -16647,3,111000030 -16648,3,112000020 -16649,3,112000030 -16650,3,108000060 -16651,3,108000070 -16652,3,108000080 -16653,3,107000050 -16654,3,112000010 -16655,3,110000070 -16656,3,110000080 -16657,3,118000020 -16658,3,118000030 -16659,3,118000040 -16660,4,101000090 -16661,4,101000100 -16662,4,101000110 -16663,4,109000100 -16664,4,105000100 -16665,4,105000110 -16666,4,108000090 -16667,4,110000090 -16668,4,102000100 -16669,4,102000110 -16670,4,106000090 -16671,4,109000090 -16672,4,107000100 -16673,4,103000090 -16674,4,102000090 -16675,4,103000100 -16676,4,106000100 -16677,4,106000110 -16678,4,104000090 -16679,4,104000100 -16680,4,104000110 -16681,4,107000090 -16682,4,104000180 -16683,4,111000040 -16684,4,112000040 -16685,4,108000100 -16686,4,105000090 -16687,4,110000100 -16688,4,118000050 -16689,4,118000060 -16690,5,101000120 -16691,5,109000110 -16692,5,105000120 -16693,5,102000120 -16694,5,107000110 -16695,5,103000120 -16696,5,106000120 -16697,5,104000120 -16698,5,104000190 -16699,5,111000060 -16700,5,112000060 -16701,5,108000110 -16702,5,110000110 -16703,5,118000070 -16704,1,201000010 -16705,1,292000010 -16706,1,299000040 -16707,1,299000070 -16708,1,299000110 -16709,1,299000120 -16710,1,299000140 -16711,2,202000010 -16712,2,290000010 -16713,2,299000010 -16714,2,299000150 -16715,2,299000190 -16716,2,299000200 -16717,2,299000210 -16718,3,298000050 -16719,3,298000060 -16720,3,299000060 -16721,3,299000170 -16722,3,290000120 -16723,3,291000050 -16724,3,292000020 -16725,4,299000670 -16726,4,299000680 -16727,4,204000010 -16728,4,209000040 -16729,4,202000070 -16730,4,209000070 -16731,4,203000110 -16732,4,290000110 -16733,4,206000110 -16734,4,209000160 -16735,4,209000190 -16736,5,297000100 -16737,5,291000020 -16738,5,297000130 -16739,5,297000140 -16740,5,203000010 -16741,5,206000030 -16742,5,203000050 -16743,5,202000090 -16744,5,204000080 -16745,5,202000150 -16746,5,204000100 -16747,5,206000170 -16748,5,204000200 -16749,3,170004 -16750,3,180004 -16751,4,140000 -16752,5,150010 -16753,5,150020 -16754,5,150030 -16755,5,150040 -16757,3,101000050 -16758,3,101000060 -16759,3,101000080 -16760,3,101000040 -16761,3,109000060 -16762,3,109000070 -16763,3,109000080 -16764,3,105000060 -16765,3,105000070 -16766,3,105000080 -16767,3,104000050 -16768,3,106000050 -16769,3,102000060 -16770,3,102000070 -16771,3,102000080 -16772,3,103000050 -16773,3,105000050 -16774,3,107000060 -16775,3,107000070 -16776,3,107000080 -16777,3,108000050 -16778,3,109000050 -16779,3,103000060 -16780,3,103000070 -16781,3,103000080 -16782,3,110000050 -16783,3,106000060 -16784,3,106000070 -16785,3,106000080 -16786,3,101000070 -16787,3,110000060 -16788,3,104000060 -16789,3,104000070 -16790,3,104000080 -16791,3,102000050 -16792,3,104000170 -16793,3,104000260 -16794,3,111000010 -16795,3,111000020 -16796,3,111000030 -16797,3,112000020 -16798,3,112000030 -16799,3,108000060 -16800,3,108000070 -16801,3,108000080 -16802,3,107000050 -16803,3,112000010 -16804,3,110000070 -16805,3,110000080 -16806,3,118000020 -16807,3,118000030 -16808,3,118000040 -16809,4,101000090 -16810,4,101000100 -16811,4,101000110 -16812,4,109000100 -16813,4,105000100 -16814,4,105000110 -16815,4,108000090 -16816,4,110000090 -16817,4,102000100 -16818,4,102000110 -16819,4,106000090 -16820,4,109000090 -16821,4,107000100 -16822,4,103000090 -16823,4,102000090 -16824,4,103000100 -16825,4,106000100 -16826,4,106000110 -16827,4,104000090 -16828,4,104000100 -16829,4,104000110 -16830,4,107000090 -16831,4,104000180 -16832,4,111000040 -16833,4,112000040 -16834,4,108000100 -16835,4,105000090 -16836,4,110000100 -16837,4,118000050 -16838,4,118000060 -16839,5,101000120 -16840,5,109000110 -16841,5,105000120 -16842,5,102000120 -16843,5,107000110 -16844,5,103000120 -16845,5,106000120 -16846,5,104000120 -16847,5,104000190 -16848,5,111000060 -16849,5,112000060 -16850,5,108000110 -16851,5,110000110 -16852,5,118000070 -16853,1,201000010 -16854,1,292000010 -16855,1,299000040 -16856,1,299000070 -16857,1,299000110 -16858,1,299000120 -16859,1,299000140 -16860,2,202000010 -16861,2,290000010 -16862,2,299000010 -16863,2,299000150 -16864,2,299000190 -16865,2,299000200 -16866,2,299000210 -16867,3,298000050 -16868,3,298000060 -16869,3,299000060 -16870,3,299000170 -16871,3,290000120 -16872,3,291000050 -16873,3,292000020 -16874,4,299000670 -16875,4,299000680 -16876,4,204000010 -16877,4,209000040 -16878,4,202000070 -16879,4,209000070 -16880,4,203000110 -16881,4,290000110 -16882,4,206000110 -16883,4,209000160 -16884,4,209000190 -16885,5,297000100 -16886,5,291000020 -16887,5,297000130 -16888,5,297000140 -16889,5,203000010 -16890,5,206000030 -16891,5,203000050 -16892,5,202000090 -16893,5,204000080 -16894,5,202000150 -16895,5,204000100 -16896,5,206000170 -16897,5,204000200 -16898,3,170004 -16899,3,180004 -16900,4,140000 -16901,5,150010 -16902,5,150020 -16903,5,150030 -16904,5,150040 -16906,3,101000050 -16907,3,101000060 -16908,3,101000080 -16909,3,101000040 -16910,3,109000060 -16911,3,109000070 -16912,3,109000080 -16913,3,105000060 -16914,3,105000070 -16915,3,105000080 -16916,3,104000050 -16917,3,106000050 -16918,3,102000060 -16919,3,102000070 -16920,3,102000080 -16921,3,103000050 -16922,3,105000050 -16923,3,107000060 -16924,3,107000070 -16925,3,107000080 -16926,3,108000050 -16927,3,109000050 -16928,3,103000060 -16929,3,103000070 -16930,3,103000080 -16931,3,110000050 -16932,3,106000060 -16933,3,106000070 -16934,3,106000080 -16935,3,101000070 -16936,3,110000060 -16937,3,104000060 -16938,3,104000070 -16939,3,104000080 -16940,3,102000050 -16941,3,104000170 -16942,3,104000260 -16943,3,111000010 -16944,3,111000020 -16945,3,111000030 -16946,3,112000020 -16947,3,112000030 -16948,3,108000060 -16949,3,108000070 -16950,3,108000080 -16951,3,107000050 -16952,3,112000010 -16953,3,110000070 -16954,3,110000080 -16955,3,118000020 -16956,3,118000030 -16957,3,118000040 -16958,4,101000090 -16959,4,101000100 -16960,4,101000110 -16961,4,109000100 -16962,4,105000100 -16963,4,105000110 -16964,4,108000090 -16965,4,110000090 -16966,4,102000100 -16967,4,102000110 -16968,4,106000090 -16969,4,109000090 -16970,4,107000100 -16971,4,103000090 -16972,4,102000090 -16973,4,103000100 -16974,4,106000100 -16975,4,106000110 -16976,4,104000090 -16977,4,104000100 -16978,4,104000110 -16979,4,107000090 -16980,4,104000180 -16981,4,111000040 -16982,4,112000040 -16983,4,108000100 -16984,4,105000090 -16985,4,110000100 -16986,4,118000050 -16987,4,118000060 -16988,5,101000120 -16989,5,109000110 -16990,5,105000120 -16991,5,102000120 -16992,5,107000110 -16993,5,103000120 -16994,5,106000120 -16995,5,104000120 -16996,5,104000190 -16997,5,111000060 -16998,5,112000060 -16999,5,108000110 -17000,5,110000110 -17001,5,118000070 -17002,1,201000010 -17003,1,292000010 -17004,1,299000040 -17005,1,299000070 -17006,1,299000110 -17007,1,299000120 -17008,1,299000140 -17009,2,202000010 -17010,2,290000010 -17011,2,299000010 -17012,2,299000150 -17013,2,299000190 -17014,2,299000200 -17015,2,299000210 -17016,3,298000050 -17017,3,298000060 -17018,3,299000060 -17019,3,299000170 -17020,3,290000120 -17021,3,291000050 -17022,3,292000020 -17023,4,299000670 -17024,4,299000680 -17025,4,204000010 -17026,4,209000040 -17027,4,202000070 -17028,4,209000070 -17029,4,203000110 -17030,4,290000110 -17031,4,206000110 -17032,4,209000160 -17033,4,209000190 -17034,5,297000100 -17035,5,291000020 -17036,5,297000130 -17037,5,297000140 -17038,5,203000010 -17039,5,206000030 -17040,5,203000050 -17041,5,202000090 -17042,5,204000080 -17043,5,202000150 -17044,5,204000100 -17045,5,206000170 -17046,5,204000200 -17047,3,170004 -17048,3,180004 -17049,4,140000 -17050,5,150010 -17051,5,150020 -17052,5,150030 -17053,5,150040 -17054,5,190000 -17055,5,200000 -17057,3,118000020 -17058,3,118000030 -17059,3,118000040 -17060,3,106000060 -17061,3,106000070 -17062,3,106000080 -17063,3,101000070 -17064,3,110000060 -17065,3,104000060 -17066,3,104000070 -17067,3,104000080 -17068,3,102000050 -17069,3,104000170 -17070,3,104000260 -17071,4,118000050 -17072,4,118000060 -17073,4,106000100 -17074,4,106000110 -17075,4,104000090 -17076,4,104000100 -17077,4,104000110 -17078,4,104000270 -17079,4,107000090 -17080,4,104000180 -17081,5,118000070 -17082,5,106000120 -17083,5,104000120 -17084,5,104000190 -17085,1,103000000 -17086,2,103000001 -17087,3,103000002 -17088,4,103000003 -17089,5,103000004 -17090,1,111000000 -17091,2,111000001 -17092,3,111000002 -17093,4,111000003 -17094,5,111000004 -17095,1,115000000 -17096,2,115000001 -17097,3,115000002 -17098,4,115000003 -17099,5,115000004 -17100,3,170004 -17101,4,170005 -17102,3,180004 -17103,4,180005 -17104,4,140000 -17105,4,150010 -17106,4,150020 -17107,4,150030 -17108,4,150040 -17110,3,111000010 -17111,3,111000020 -17112,3,111000030 -17113,3,112000020 -17114,3,112000030 -17115,3,108000060 -17116,3,108000070 -17117,3,108000080 -17118,3,107000050 -17119,3,112000010 -17120,3,110000070 -17121,3,110000080 -17122,4,111000040 -17123,4,112000040 -17124,4,108000100 -17125,4,105000090 -17126,4,110000100 -17127,5,111000060 -17128,5,112000060 -17129,5,108000110 -17130,5,110000110 -17131,1,108000000 -17132,2,108000001 -17133,3,108000002 -17134,4,108000003 -17135,5,108000004 -17136,1,107000000 -17137,2,107000001 -17138,3,107000002 -17139,4,107000003 -17140,5,107000004 -17141,1,120000000 -17142,2,120000001 -17143,3,120000002 -17144,4,120000003 -17145,5,120000004 -17146,4,110024 -17147,4,110034 -17148,4,110044 -17149,4,110054 -17150,3,110060 -17151,3,110070 -17152,3,110080 -17153,3,110090 -17154,3,110100 -17155,3,110110 -17156,3,110120 -17157,3,110130 -17158,3,110140 -17159,3,110150 -17160,3,110160 -17161,3,110170 -17162,3,110180 -17163,3,110190 -17164,3,110200 -17165,3,110210 -17166,3,110220 -17167,3,110230 -17168,3,110240 -17169,3,110250 -17170,3,110260 -17171,3,110270 -17172,3,110620 -17173,3,110670 -17174,4,140000 -17175,4,150010 -17176,4,150020 -17177,4,150030 -17178,4,150040 -17180,3,101000050 -17181,3,101000060 -17182,3,101000080 -17183,3,101000040 -17184,3,109000060 -17185,3,109000070 -17186,3,109000080 -17187,3,105000060 -17188,3,105000070 -17189,3,105000080 -17190,3,104000050 -17191,3,106000050 -17192,4,101000090 -17193,4,101000100 -17194,4,101000110 -17195,4,109000100 -17196,4,105000100 -17197,4,105000110 -17198,4,108000090 -17199,4,110000090 -17200,5,101000120 -17201,5,109000110 -17202,5,105000120 -17203,1,101000000 -17204,2,101000001 -17205,3,101000002 -17206,4,101000008 -17207,5,101000004 -17208,1,109000000 -17209,2,109000001 -17210,3,109000002 -17211,4,109000003 -17212,5,109000004 -17213,4,120024 -17214,4,120034 -17215,4,120044 -17216,4,120054 -17217,3,120241 -17218,3,120251 -17219,3,120261 -17220,3,120271 -17221,3,120300 -17222,3,120310 -17223,3,120320 -17224,3,120330 -17225,3,120340 -17226,3,120350 -17227,3,120360 -17228,3,120370 -17229,3,120380 -17230,3,120390 -17231,3,120400 -17232,3,120410 -17233,3,120420 -17234,3,120430 -17235,3,120450 -17236,3,120460 -17237,3,120550 -17238,3,120560 -17239,3,120570 -17240,3,120990 -17241,3,121000 -17242,3,121010 -17243,3,121020 -17244,3,121100 -17245,4,140000 -17246,4,150010 -17247,4,150020 -17248,4,150030 -17249,4,150040 -17251,3,102000060 -17252,3,102000070 -17253,3,102000080 -17254,3,103000050 -17255,3,105000050 -17256,3,107000060 -17257,3,107000070 -17258,3,107000080 -17259,3,108000050 -17260,3,109000050 -17261,3,103000060 -17262,3,103000070 -17263,3,103000080 -17264,3,110000050 -17265,4,102000100 -17266,4,102000110 -17267,4,102000350 -17268,4,106000090 -17269,4,109000090 -17270,4,107000100 -17271,4,103000090 -17272,4,102000090 -17273,4,103000100 -17274,5,102000120 -17275,5,107000110 -17276,5,103000120 -17277,1,102000000 -17278,2,102000001 -17279,3,102000002 -17280,4,102000003 -17281,5,102000004 -17282,1,105000000 -17283,2,105000001 -17284,3,105000002 -17285,4,105000003 -17286,5,105000004 -17287,1,112000000 -17288,2,112000001 -17289,3,112000002 -17290,4,112000003 -17291,5,112000004 -17292,4,130024 -17293,4,130034 -17294,4,130044 -17295,4,130054 -17296,3,130060 -17297,3,130070 -17298,3,130080 -17299,3,130090 -17300,3,130100 -17301,3,130110 -17302,3,130120 -17303,3,130130 -17304,3,130140 -17305,3,130150 -17306,3,130160 -17307,3,130170 -17308,3,130180 -17309,3,130190 -17310,3,130200 -17311,3,130420 -17312,3,130510 -17313,3,130520 -17314,3,130531 -17315,3,130540 -17316,3,130660 -17317,3,130700 -17318,3,130790 -17319,3,130800 -17320,3,131130 -17321,4,140000 -17322,4,150010 -17323,4,150020 -17324,4,150030 -17325,4,150040 -17327,4,101000250 -17328,4,102000260 -17329,4,103000220 -17330,4,104000250 -17331,4,105000210 -17332,4,106000210 -17333,4,107000140 -17334,4,108000150 -17335,4,109000230 -17336,4,110000170 -17337,4,111000140 -17338,4,112000110 -17339,4,118000140 -17340,5,101000300 -17341,5,102000360 -17342,5,103000310 -17343,5,104000370 -17344,5,109000290 -17345,5,101000310 -17346,5,102000410 -17347,1,201000010 -17348,1,292000010 -17349,1,299000040 -17350,1,299000070 -17351,1,299000110 -17352,1,299000120 -17353,1,299000140 -17354,2,202000010 -17355,2,290000010 -17356,2,299000010 -17357,2,299000150 -17358,2,299000190 -17359,2,299000200 -17360,2,299000210 -17361,3,290000120 -17362,3,291000050 -17363,3,292000020 -17364,3,298000050 -17365,3,298000060 -17366,3,299000060 -17367,3,299000170 -17368,4,201000170 -17369,4,202000070 -17370,4,202000370 -17371,4,202000400 -17372,4,202000440 -17373,4,203000110 -17374,4,203000270 -17375,4,203000350 -17376,4,204000010 -17377,4,204000370 -17378,4,205000220 -17379,4,206000110 -17380,4,206000240 -17381,4,206000270 -17382,4,209000040 -17383,4,209000070 -17384,4,209000160 -17385,4,209000190 -17386,4,209000240 -17387,4,215000150 -17388,4,290000110 -17389,4,290000130 -17390,4,298000120 -17391,4,299000670 -17392,4,299000680 -17393,4,201000090 -17394,4,202000160 -17395,4,203000150 -17396,4,204000110 -17397,4,205000110 -17398,4,206000120 -17399,4,211000060 -17400,4,212000050 -17401,4,299000440 -17402,4,299000450 -17403,4,299000460 -17404,4,299000470 -17405,4,299000480 -17406,4,299000550 -17407,4,299000560 -17408,4,299000570 -17409,4,299000580 -17410,4,299000590 -17411,4,299000600 -17412,4,299000610 -17413,4,299000620 -17414,4,299000630 -17415,4,299000640 -17416,4,299000650 -17417,4,299000230 -17418,4,299000240 -17419,4,299000250 -17420,4,299000260 -17421,4,299000270 -17422,4,299000280 -17423,4,299000290 -17424,4,299000300 -17425,4,299000310 -17426,4,299000320 -17427,4,299000330 -17428,4,299000340 -17429,4,299000350 -17430,4,299000360 -17431,4,299000370 -17432,4,299000380 -17433,4,299000390 -17434,4,299000400 -17435,4,215000050 -17436,4,216000060 -17437,4,217000020 -17438,4,218000030 -17439,4,299000500 -17440,4,299000510 -17441,4,299000520 -17442,4,299000530 -17443,4,299000540 -17444,4,299000660 -17445,5,202000090 -17446,5,202000150 -17447,5,203000010 -17448,5,203000050 -17449,5,203000260 -17450,5,204000080 -17451,5,204000100 -17452,5,204000200 -17453,5,204000270 -17454,5,206000030 -17455,5,206000170 -17456,5,209000320 -17457,5,220000080 -17458,5,290000160 -17459,5,291000020 -17460,5,297000100 -17461,5,297000130 -17462,5,297000140 -17463,5,298000110 -17464,5,202000290 -17465,5,203000240 -17466,5,204000210 -17467,5,205000150 -17468,5,206000200 -17469,5,211000090 -17470,5,212000060 -17471,5,299000800 -17472,5,299000810 -17473,5,299000820 -17474,5,299000840 -17475,5,299000850 -17476,5,299000860 -17477,5,299000870 -17478,5,299000880 -17479,5,299000890 -17480,5,201000120 -17481,5,202000240 -17482,5,211000080 -17483,5,299000730 -17484,5,299000740 -17485,5,299000750 -17486,5,299000760 -17487,5,299000770 -17488,5,201000130 -17489,5,299000830 -17490,5,202000280 -17491,5,204000220 -17492,5,201000150 -17493,5,202000350 -17494,5,205000200 -17495,5,299001050 -17496,5,299001060 -17497,5,201000200 -17498,5,202000430 -17499,5,211000150 -17500,5,299001170 -17501,5,299001180 -17502,5,202000270 -17503,5,206000190 -17504,5,220000060 -17505,5,201000140 -17506,5,203000230 -17507,5,205000160 -17508,5,204000230 -17509,5,209000200 -17510,5,299000900 -17511,5,202000340 -17512,5,203000280 -17513,5,205000190 -17514,5,208000040 -17515,5,211000110 -17516,5,220000070 -17517,5,202000420 -17518,5,203000340 -17519,5,204000360 -17520,5,211000140 -17521,5,212000090 -17522,5,299000920 -17523,5,299000930 -17524,5,299000940 -17525,5,299000950 -17526,5,299000960 -17527,5,298000130 -17528,5,202000380 -17529,5,203000300 -17530,5,204000320 -17531,5,205000230 -17532,5,206000250 -17533,5,209000280 -17534,5,210000030 -17535,5,211000120 -17536,5,212000070 -17537,5,215000130 -17538,5,216000130 -17539,5,217000080 -17540,5,218000090 -17541,5,299001070 -17542,5,299001080 -17543,5,299001090 -17544,5,215000120 -17545,5,202000390 -17546,5,203000310 -17547,5,204000330 -17548,5,205000240 -17549,5,206000260 -17550,5,209000290 -17551,5,210000040 -17552,5,211000130 -17553,5,212000080 -17554,5,215000140 -17555,5,216000140 -17556,5,217000090 -17557,5,218000100 -17558,5,220000090 -17559,5,299001100 -17560,5,299001110 -17561,5,299001120 -17562,5,299001130 -17563,5,299001140 -17564,5,299001150 -17565,5,299000430 -17566,5,299000690 -17567,5,299000710 -17568,5,299000720 -17569,5,215000100 -17570,5,216000090 -17571,5,217000070 -17572,5,218000080 -17573,5,299000970 -17574,5,299000980 -17575,5,299000990 -17576,5,299001000 -17577,5,299001010 -17578,5,299001020 -17579,5,299001030 -17580,5,299001040 -17581,3,101000006 -17582,3,103000005 -17583,4,101000003 -17584,4,102000005 -17585,4,107000005 -17586,4,112000006 -17587,5,101000009 -17588,5,101000011 -17589,5,101000012 -17590,5,101000013 -17591,5,101000014 -17592,5,101000015 -17593,5,101000016 -17594,5,101000017 -17595,5,101000018 -17596,5,102000008 -17597,5,102000009 -17598,5,107000007 -17599,5,109000008 -17600,5,111000007 -17601,5,111000008 -17602,5,112000008 -17603,5,120000008 -17605,2,118000010 -17606,3,118000020 -17607,3,118000030 -17608,3,118000040 -17609,4,118000050 -17610,4,118000060 -17611,5,118000070 -17612,1,101000000 -17613,3,101000006 -17614,3,103000005 -17615,4,101000003 -17616,4,102000005 -17617,4,107000005 -17618,4,112000006 -17619,5,101000009 -17620,5,101000011 -17621,5,101000012 -17622,5,101000013 -17623,5,101000014 -17624,5,101000015 -17625,5,101000016 -17626,5,101000017 -17627,5,101000018 -17628,5,102000008 -17629,5,102000009 -17630,5,107000007 -17631,5,109000008 -17632,5,111000007 -17633,5,111000008 -17634,5,112000008 -17635,5,120000008 -17636,3,110540 -17637,3,110680 -17638,3,110790 -17639,3,110800 -17640,3,120440 -17641,5,110055 -17642,5,110241 -17643,5,110251 -17644,5,110261 -17645,5,110271 -17646,5,110730 -17647,5,111020 -17648,5,111100 -17649,5,111160 -17650,5,120551 -17651,5,121160 +RewardTableId,UnanalyzedLogGradeId,CommonRewardId,CommonRewardType +142,1,101000000,2 +143,2,101000001,2 +144,3,101000002,2 +145,4,101000008,2 +146,5,101000004,2 +147,1,102000000,2 +148,2,102000001,2 +149,3,102000002,2 +150,4,102000003,2 +151,5,102000004,2 +152,1,103000000,2 +153,2,103000001,2 +154,3,103000002,2 +155,4,103000003,2 +156,5,103000004,2 +157,1,105000000,2 +158,2,105000001,2 +159,3,105000002,2 +160,4,105000003,2 +161,5,105000004,2 +162,1,108000000,2 +163,2,108000001,2 +164,3,108000002,2 +165,4,108000003,2 +166,5,108000004,2 +167,1,109000000,2 +168,2,109000001,2 +169,3,109000002,2 +170,4,109000003,2 +171,5,109000004,2 +172,1,111000000,2 +173,2,111000001,2 +174,3,111000002,2 +175,4,111000003,2 +176,5,111000004,2 +177,1,112000000,2 +178,2,112000001,2 +179,3,112000002,2 +180,4,112000003,2 +181,5,112000004,2 +182,1,120000000,2 +183,2,120000001,2 +184,3,120000002,2 +185,4,120000003,2 +186,5,120000004,2 +187,1,101000010,1 +188,2,101000030,1 +189,3,101000040,1 +190,1,102000010,1 +191,2,102000030,1 +192,3,102000070,1 +193,1,103000010,1 +194,2,103000040,1 +195,3,103000070,1 +196,1,104000010,1 +197,2,104000030,1 +198,3,104000070,1 +199,1,105000010,1 +200,2,105000040,1 +201,3,105000070,1 +202,1,106000010,1 +203,2,106000030,1 +204,3,106000070,1 +205,1,107000010,1 +206,2,107000040,1 +207,3,107000070,1 +208,1,108000010,1 +209,2,108000030,1 +210,3,108000050,1 +212,1,101000000,2 +213,2,101000001,2 +214,3,101000002,2 +215,4,101000008,2 +216,5,101000004,2 +218,1,101000000,2 +219,2,101000001,2 +220,3,101000002,2 +221,4,101000008,2 +222,5,101000004,2 +223,1,102000000,2 +224,2,102000001,2 +225,3,102000002,2 +226,4,102000003,2 +227,5,102000004,2 +228,1,103000000,2 +229,2,103000001,2 +230,3,103000002,2 +231,4,103000003,2 +232,5,103000004,2 +233,1,105000000,2 +234,2,105000001,2 +235,3,105000002,2 +236,4,105000003,2 +237,5,105000004,2 +238,1,108000000,2 +239,2,108000001,2 +240,3,108000002,2 +241,4,108000003,2 +242,5,108000004,2 +243,1,109000000,2 +244,2,109000001,2 +245,3,109000002,2 +246,4,109000003,2 +247,5,109000004,2 +248,1,111000000,2 +249,2,111000001,2 +250,3,111000002,2 +251,4,111000003,2 +252,5,111000004,2 +253,1,112000000,2 +254,2,112000001,2 +255,3,112000002,2 +256,4,112000003,2 +257,5,112000004,2 +258,1,120000000,2 +259,2,120000001,2 +260,3,120000002,2 +261,4,120000003,2 +262,5,120000004,2 +263,1,101000010,1 +264,2,101000020,1 +265,2,101000030,1 +266,3,101000040,1 +267,3,101000050,1 +268,3,101000060,1 +269,3,101000070,1 +270,3,101000080,1 +271,1,102000010,1 +272,2,102000020,1 +273,2,102000030,1 +274,2,102000040,1 +275,3,102000050,1 +276,3,102000060,1 +277,3,102000070,1 +278,3,102000080,1 +279,1,103000010,1 +280,2,103000020,1 +281,2,103000030,1 +282,2,103000040,1 +283,3,103000050,1 +284,3,103000060,1 +285,3,103000070,1 +286,3,103000080,1 +287,1,104000010,1 +288,2,104000020,1 +289,2,104000030,1 +290,2,104000040,1 +291,3,104000050,1 +292,3,104000060,1 +293,3,104000070,1 +294,3,104000080,1 +295,1,105000010,1 +296,2,105000020,1 +297,2,105000030,1 +298,2,105000040,1 +299,3,105000050,1 +300,3,105000060,1 +301,3,105000070,1 +302,3,105000080,1 +303,1,106000010,1 +304,2,106000020,1 +305,2,106000030,1 +306,2,106000040,1 +307,3,106000050,1 +308,3,106000060,1 +309,3,106000070,1 +310,3,106000080,1 +311,1,107000010,1 +312,2,107000020,1 +313,2,107000030,1 +314,2,107000040,1 +315,3,107000050,1 +316,3,107000060,1 +317,3,107000070,1 +318,3,107000080,1 +319,1,108000010,1 +320,2,108000020,1 +321,2,108000030,1 +322,2,108000040,1 +323,3,108000050,1 +324,3,108000060,1 +325,3,108000070,1 +326,3,108000080,1 +327,2,180000,3 +329,1,101000000,2 +330,2,101000001,2 +331,3,101000002,2 +332,4,101000008,2 +333,5,101000004,2 +334,1,102000000,2 +335,2,102000001,2 +336,3,102000002,2 +337,4,102000003,2 +338,5,102000004,2 +339,1,103000000,2 +340,2,103000001,2 +341,3,103000002,2 +342,4,103000003,2 +343,5,103000004,2 +344,1,105000000,2 +345,2,105000001,2 +346,3,105000002,2 +347,4,105000003,2 +348,5,105000004,2 +349,1,108000000,2 +350,2,108000001,2 +351,3,108000002,2 +352,4,108000003,2 +353,5,108000004,2 +354,1,109000000,2 +355,2,109000001,2 +356,3,109000002,2 +357,4,109000003,2 +358,5,109000004,2 +359,1,111000000,2 +360,2,111000001,2 +361,3,111000002,2 +362,4,111000003,2 +363,5,111000004,2 +364,1,112000000,2 +365,2,112000001,2 +366,3,112000002,2 +367,4,112000003,2 +368,5,112000004,2 +369,1,120000000,2 +370,2,120000001,2 +371,3,120000002,2 +372,4,120000003,2 +373,5,120000004,2 +374,1,101000010,1 +375,2,101000020,1 +376,2,101000030,1 +377,3,101000040,1 +378,3,101000050,1 +379,3,101000060,1 +380,3,101000070,1 +381,3,101000080,1 +382,1,102000010,1 +383,2,102000020,1 +384,2,102000030,1 +385,2,102000040,1 +386,3,102000050,1 +387,3,102000060,1 +388,3,102000070,1 +389,3,102000080,1 +390,1,103000010,1 +391,2,103000020,1 +392,2,103000030,1 +393,2,103000040,1 +394,3,103000050,1 +395,3,103000060,1 +396,3,103000070,1 +397,3,103000080,1 +398,1,104000010,1 +399,2,104000020,1 +400,2,104000030,1 +401,2,104000040,1 +402,3,104000050,1 +403,3,104000060,1 +404,3,104000070,1 +405,3,104000080,1 +406,1,105000010,1 +407,2,105000020,1 +408,2,105000030,1 +409,2,105000040,1 +410,3,105000050,1 +411,3,105000060,1 +412,3,105000070,1 +413,3,105000080,1 +414,1,106000010,1 +415,2,106000020,1 +416,2,106000030,1 +417,2,106000040,1 +418,3,106000050,1 +419,3,106000060,1 +420,3,106000070,1 +421,3,106000080,1 +422,1,107000010,1 +423,2,107000020,1 +424,2,107000030,1 +425,2,107000040,1 +426,3,107000050,1 +427,3,107000060,1 +428,3,107000070,1 +429,3,107000080,1 +430,1,108000010,1 +431,2,108000020,1 +432,2,108000030,1 +433,2,108000040,1 +434,3,108000050,1 +435,3,108000060,1 +436,3,108000070,1 +437,3,108000080,1 +438,2,180000,3 +440,1,101000000,2 +441,2,101000001,2 +442,3,101000002,2 +443,4,101000008,2 +444,5,101000004,2 +446,1,102000000,2 +447,2,102000001,2 +448,3,102000002,2 +449,4,102000003,2 +450,5,102000004,2 +451,1,112000000,2 +452,2,112000001,2 +453,3,112000002,2 +454,4,112000003,2 +455,5,112000004,2 +457,1,101000000,2 +458,2,101000001,2 +459,3,101000002,2 +460,4,101000008,2 +461,5,101000004,2 +462,1,120000000,2 +463,2,120000001,2 +464,3,120000002,2 +465,4,120000003,2 +466,5,120000004,2 +468,1,111000000,2 +469,2,111000001,2 +470,3,111000002,2 +471,4,111000003,2 +472,5,111000004,2 +473,1,120000000,2 +474,2,120000001,2 +475,3,120000002,2 +476,4,120000003,2 +477,5,120000004,2 +479,1,109000000,2 +480,2,109000001,2 +481,3,109000002,2 +482,4,109000003,2 +483,5,109000004,2 +484,1,112000000,2 +485,2,112000001,2 +486,3,112000002,2 +487,4,112000003,2 +488,5,112000004,2 +490,1,103000000,2 +491,2,103000001,2 +492,3,103000002,2 +493,4,103000003,2 +494,5,103000004,2 +495,1,112000000,2 +496,2,112000001,2 +497,3,112000002,2 +498,4,112000003,2 +499,5,112000004,2 +501,1,105000000,2 +502,2,105000001,2 +503,3,105000002,2 +504,4,105000003,2 +505,5,105000004,2 +506,1,120000000,2 +507,2,120000001,2 +508,3,120000002,2 +509,4,120000003,2 +510,5,120000004,2 +512,1,108000000,2 +513,2,108000001,2 +514,3,108000002,2 +515,4,108000003,2 +516,5,108000004,2 +517,1,120000000,2 +518,2,120000001,2 +519,3,120000002,2 +520,4,120000003,2 +521,5,120000004,2 +523,1,101000010,1 +524,1,103000010,1 +525,2,103000030,1 +526,2,103000040,1 +527,2,107000020,1 +528,2,101000030,1 +529,3,101000050,1 +530,3,101000060,1 +531,3,101000080,1 +532,3,103000060,1 +533,3,103000070,1 +534,3,103000080,1 +535,3,101000040,1 +536,1,101000000,2 +537,2,101000001,2 +538,3,101000002,2 +539,4,101000008,2 +540,5,101000004,2 +541,1,112000000,2 +542,2,112000001,2 +543,3,112000002,2 +544,4,112000003,2 +545,5,112000004,2 +546,2,101000005,2 +547,2,112000005,2 +548,3,170000,3 +549,4,170001,3 +551,1,102000010,1 +552,2,102000030,1 +553,2,102000040,1 +554,2,104000020,1 +555,3,102000060,1 +556,3,102000070,1 +557,3,102000080,1 +558,3,103000050,1 +559,3,105000050,1 +560,1,102000000,2 +561,2,102000001,2 +562,3,102000002,2 +563,4,102000003,2 +564,5,102000004,2 +565,1,112000000,2 +566,2,112000001,2 +567,3,112000002,2 +568,4,112000003,2 +569,5,112000004,2 +570,3,170000,3 +571,4,170001,3 +573,1,106000010,1 +574,2,106000030,1 +575,2,106000040,1 +576,2,105000020,1 +577,3,106000060,1 +578,3,106000070,1 +579,3,106000080,1 +580,3,101000070,1 +581,1,103000000,2 +582,2,103000001,2 +583,3,103000002,2 +584,4,103000003,2 +585,5,103000004,2 +586,1,112000000,2 +587,2,112000001,2 +588,3,112000002,2 +589,4,112000003,2 +590,5,112000004,2 +591,3,170000,3 +592,4,170001,3 +594,1,104000010,1 +595,2,104000030,1 +596,2,104000040,1 +597,2,108000020,1 +598,3,104000060,1 +599,3,104000070,1 +600,3,104000080,1 +601,3,102000050,1 +602,1,111000000,2 +603,2,111000001,2 +604,3,111000002,2 +605,4,111000003,2 +606,5,111000004,2 +607,1,120000000,2 +608,2,120000001,2 +609,3,120000002,2 +610,4,120000003,2 +611,5,120000004,2 +612,1,110020,3 +613,1,110030,3 +614,1,110040,3 +615,1,110050,3 +616,3,110060,3 +617,3,110070,3 +618,3,110080,3 +619,3,110090,3 +620,3,110100,3 +621,3,110110,3 +622,3,110120,3 +623,3,110130,3 +624,3,110140,3 +625,3,110150,3 +626,3,110160,3 +627,3,110170,3 +628,3,110180,3 +629,3,110190,3 +630,3,110200,3 +631,3,110210,3 +632,3,110220,3 +633,3,110230,3 +634,3,110240,3 +635,3,110250,3 +636,3,110260,3 +637,3,110270,3 +639,1,107000010,1 +640,2,107000030,1 +641,2,107000040,1 +642,2,101000020,1 +643,3,107000060,1 +644,3,107000070,1 +645,3,107000080,1 +646,3,108000050,1 +647,1,105000000,2 +648,2,105000001,2 +649,3,105000002,2 +650,4,105000003,2 +651,5,105000004,2 +652,1,120000000,2 +653,2,120000001,2 +654,3,120000002,2 +655,4,120000003,2 +656,5,120000004,2 +657,3,180000,3 +658,4,180001,3 +660,1,105000010,1 +661,2,105000030,1 +662,2,105000040,1 +663,2,102000020,1 +664,2,103000020,1 +665,3,105000060,1 +666,3,105000070,1 +667,3,105000080,1 +668,3,104000050,1 +669,3,106000050,1 +670,1,109000000,2 +671,2,109000001,2 +672,3,109000002,2 +673,4,109000003,2 +674,5,109000004,2 +675,1,112000000,2 +676,2,112000001,2 +677,3,112000002,2 +678,4,112000003,2 +679,5,112000004,2 +680,1,120020,3 +681,1,120030,3 +682,1,120040,3 +683,1,120050,3 +684,3,120240,3 +685,3,120250,3 +686,3,120260,3 +687,3,120270,3 +688,3,120300,3 +689,3,120310,3 +690,3,120320,3 +691,3,120330,3 +692,3,120340,3 +693,3,120350,3 +694,3,120360,3 +695,3,120370,3 +696,3,120380,3 +697,3,120390,3 +698,3,120400,3 +699,3,120410,3 +700,3,120420,3 +701,3,120430,3 +702,3,120450,3 +703,3,120460,3 +705,1,108000010,1 +706,2,108000030,1 +707,2,108000040,1 +708,2,106000020,1 +709,3,108000060,1 +710,3,108000070,1 +711,3,108000080,1 +712,3,107000050,1 +713,1,108000000,2 +714,2,108000001,2 +715,3,108000002,2 +716,4,108000003,2 +717,5,108000004,2 +718,1,120000000,2 +719,2,120000001,2 +720,3,120000002,2 +721,4,120000003,2 +722,5,120000004,2 +723,1,130020,3 +724,1,130030,3 +725,1,130040,3 +726,1,130050,3 +727,3,130060,3 +728,3,130070,3 +729,3,130080,3 +730,3,130090,3 +731,3,130100,3 +732,3,130110,3 +733,3,130120,3 +734,3,130130,3 +735,3,130140,3 +736,3,130150,3 +737,3,130160,3 +738,3,130170,3 +739,3,130180,3 +740,3,130190,3 +741,3,130200,3 +742,3,130420,3 +743,3,130510,3 +744,3,130520,3 +745,3,130530,3 +746,3,130540,3 +748,1,105000010,1 +749,2,105000030,1 +750,2,105000040,1 +751,2,102000020,1 +752,2,103000020,1 +753,3,105000060,1 +754,3,105000070,1 +755,3,105000080,1 +756,3,104000050,1 +757,3,106000050,1 +758,1,109000000,2 +759,2,109000001,2 +760,3,109000002,2 +761,4,109000003,2 +762,5,109000004,2 +763,1,112000000,2 +764,2,112000001,2 +765,3,112000002,2 +766,4,112000003,2 +767,5,112000004,2 +768,3,170000,3 +769,4,170001,3 +771,1,104000010,1 +772,2,104000030,1 +773,2,104000040,1 +774,2,108000020,1 +775,3,104000060,1 +776,3,104000070,1 +777,3,104000080,1 +778,3,102000050,1 +779,1,111000000,2 +780,2,111000001,2 +781,3,111000002,2 +782,4,111000003,2 +783,5,111000004,2 +784,1,120000000,2 +785,2,120000001,2 +786,3,120000002,2 +787,4,120000003,2 +788,5,120000004,2 +789,1,110020,3 +790,1,110030,3 +791,1,110040,3 +792,1,110050,3 +793,3,110060,3 +794,3,110070,3 +795,3,110080,3 +796,3,110090,3 +797,3,110100,3 +798,3,110110,3 +799,3,110120,3 +800,3,110130,3 +801,3,110140,3 +802,3,110150,3 +803,3,110160,3 +804,3,110170,3 +805,3,110180,3 +806,3,110190,3 +807,3,110200,3 +808,3,110210,3 +809,3,110220,3 +810,3,110230,3 +811,3,110240,3 +812,3,110250,3 +813,3,110260,3 +814,3,110270,3 +816,1,102000010,1 +817,2,102000030,1 +818,2,102000040,1 +819,2,104000020,1 +820,3,102000060,1 +821,3,102000070,1 +822,3,102000080,1 +823,3,103000050,1 +824,3,105000050,1 +825,1,102000000,2 +826,2,102000001,2 +827,3,102000002,2 +828,4,102000003,2 +829,5,102000004,2 +830,1,112000000,2 +831,2,112000001,2 +832,3,112000002,2 +833,4,112000003,2 +834,5,112000004,2 +835,3,170001,3 +836,4,170002,3 +838,1,102000010,1 +839,2,102000030,1 +840,2,102000040,1 +841,2,104000020,1 +842,3,102000060,1 +843,3,102000070,1 +844,3,102000080,1 +845,3,103000050,1 +846,3,105000050,1 +847,1,102000000,2 +848,2,102000001,2 +849,3,102000002,2 +850,4,102000003,2 +851,5,102000004,2 +852,1,112000000,2 +853,2,112000001,2 +854,3,112000002,2 +855,4,112000003,2 +856,5,112000004,2 +857,1,110020,3 +858,1,110030,3 +859,1,110040,3 +860,1,110050,3 +861,2,110021,3 +862,2,110031,3 +863,2,110041,3 +864,2,110051,3 +865,3,110060,3 +866,3,110070,3 +867,3,110080,3 +868,3,110090,3 +869,3,110100,3 +870,3,110110,3 +871,3,110120,3 +872,3,110130,3 +873,3,110140,3 +874,3,110150,3 +875,3,110160,3 +876,3,110170,3 +877,3,110180,3 +878,3,110190,3 +879,3,110200,3 +880,3,110210,3 +881,3,110220,3 +882,3,110230,3 +883,3,110240,3 +884,3,110250,3 +885,3,110260,3 +886,3,110270,3 +887,4,140000,3 +889,1,101000010,1 +890,1,103000010,1 +891,2,103000030,1 +892,2,103000040,1 +893,2,107000020,1 +894,2,101000030,1 +895,3,101000050,1 +896,3,101000060,1 +897,3,101000080,1 +898,3,103000060,1 +899,3,103000070,1 +900,3,103000080,1 +901,3,101000040,1 +902,1,101000000,2 +903,2,101000001,2 +904,3,101000002,2 +905,4,101000008,2 +906,5,101000004,2 +907,1,112000000,2 +908,2,112000001,2 +909,3,112000002,2 +910,4,112000003,2 +911,5,112000004,2 +912,1,120020,3 +913,1,120030,3 +914,1,120040,3 +915,1,120050,3 +916,2,120021,3 +917,2,120031,3 +918,2,120041,3 +919,2,120051,3 +920,3,120240,3 +921,3,120250,3 +922,3,120260,3 +923,3,120270,3 +924,3,120300,3 +925,3,120310,3 +926,3,120320,3 +927,3,120330,3 +928,3,120340,3 +929,3,120350,3 +930,3,120360,3 +931,3,120370,3 +932,3,120380,3 +933,3,120390,3 +934,3,120400,3 +935,3,120410,3 +936,3,120420,3 +937,3,120430,3 +938,3,120450,3 +939,3,120460,3 +940,4,140000,3 +942,1,106000010,1 +943,2,106000030,1 +944,2,106000040,1 +945,2,105000020,1 +946,3,106000060,1 +947,3,106000070,1 +948,3,106000080,1 +949,3,101000070,1 +950,1,103000000,2 +951,2,103000001,2 +952,3,103000002,2 +953,4,103000003,2 +954,5,103000004,2 +955,1,112000000,2 +956,2,112000001,2 +957,3,112000002,2 +958,4,112000003,2 +959,5,112000004,2 +960,3,180001,3 +961,4,180002,3 +963,1,101000010,1 +964,1,103000010,1 +965,2,103000030,1 +966,2,103000040,1 +967,2,107000020,1 +968,2,101000030,1 +969,3,101000050,1 +970,3,101000060,1 +971,3,101000080,1 +972,3,103000060,1 +973,3,103000070,1 +974,3,103000080,1 +975,3,101000040,1 +976,1,101000000,2 +977,2,101000001,2 +978,3,101000002,2 +979,4,101000008,2 +980,5,101000004,2 +981,1,120000000,2 +982,2,120000001,2 +983,3,120000002,2 +984,4,120000003,2 +985,5,120000004,2 +986,3,170001,3 +987,4,170002,3 +989,1,105000010,1 +990,2,105000030,1 +991,2,105000040,1 +992,2,102000020,1 +993,2,103000020,1 +994,3,105000060,1 +995,3,105000070,1 +996,3,105000080,1 +997,3,104000050,1 +998,3,106000050,1 +999,1,109000000,2 +1000,2,109000001,2 +1001,3,109000002,2 +1002,4,109000003,2 +1003,5,109000004,2 +1004,1,112000000,2 +1005,2,112000001,2 +1006,3,112000002,2 +1007,4,112000003,2 +1008,5,112000004,2 +1009,1,130020,3 +1010,1,130030,3 +1011,1,130040,3 +1012,1,130050,3 +1013,2,130021,3 +1014,2,130031,3 +1015,2,130041,3 +1016,2,130051,3 +1017,3,130060,3 +1018,3,130070,3 +1019,3,130080,3 +1020,3,130090,3 +1021,3,130100,3 +1022,3,130110,3 +1023,3,130120,3 +1024,3,130130,3 +1025,3,130140,3 +1026,3,130150,3 +1027,3,130160,3 +1028,3,130170,3 +1029,3,130180,3 +1030,3,130190,3 +1031,3,130200,3 +1032,3,130420,3 +1033,3,130510,3 +1034,3,130520,3 +1035,3,130530,3 +1036,3,130540,3 +1037,4,140000,3 +1039,1,107000010,1 +1040,2,107000030,1 +1041,2,107000040,1 +1042,2,101000020,1 +1043,3,107000060,1 +1044,3,107000070,1 +1045,3,107000080,1 +1046,3,108000050,1 +1047,1,105000000,2 +1048,2,105000001,2 +1049,3,105000002,2 +1050,4,105000003,2 +1051,5,105000004,2 +1052,1,120000000,2 +1053,2,120000001,2 +1054,3,120000002,2 +1055,4,120000003,2 +1056,5,120000004,2 +1057,1,120020,3 +1058,1,120030,3 +1059,1,120040,3 +1060,1,120050,3 +1061,2,120021,3 +1062,2,120031,3 +1063,2,120041,3 +1064,2,120051,3 +1065,3,120240,3 +1066,3,120250,3 +1067,3,120260,3 +1068,3,120270,3 +1069,3,120300,3 +1070,3,120310,3 +1071,3,120320,3 +1072,3,120330,3 +1073,3,120340,3 +1074,3,120350,3 +1075,3,120360,3 +1076,3,120370,3 +1077,3,120380,3 +1078,3,120390,3 +1079,3,120400,3 +1080,3,120410,3 +1081,3,120420,3 +1082,3,120430,3 +1083,3,120450,3 +1084,3,120460,3 +1085,4,140000,3 +1087,1,108000010,1 +1088,2,108000030,1 +1089,2,108000040,1 +1090,2,106000020,1 +1091,3,108000060,1 +1092,3,108000070,1 +1093,3,108000080,1 +1094,3,107000050,1 +1095,1,108000000,2 +1096,2,108000001,2 +1097,3,108000002,2 +1098,4,108000003,2 +1099,5,108000004,2 +1100,1,120000000,2 +1101,2,120000001,2 +1102,3,120000002,2 +1103,4,120000003,2 +1104,5,120000004,2 +1105,3,170001,3 +1106,4,170002,3 +1108,1,101000010,1 +1109,1,103000010,1 +1110,2,103000030,1 +1111,2,103000040,1 +1112,2,107000020,1 +1113,2,101000030,1 +1114,3,101000050,1 +1115,3,101000060,1 +1116,3,101000080,1 +1117,3,103000060,1 +1118,3,103000070,1 +1119,3,103000080,1 +1120,3,101000040,1 +1121,1,101000000,2 +1122,2,101000001,2 +1123,3,101000002,2 +1124,4,101000008,2 +1125,5,101000004,2 +1126,1,112000000,2 +1127,2,112000001,2 +1128,3,112000002,2 +1129,4,112000003,2 +1130,5,112000004,2 +1131,2,101000005,2 +1132,2,112000005,2 +1133,3,170001,3 +1134,4,170002,3 +1136,1,102000010,1 +1137,2,102000030,1 +1138,2,102000040,1 +1139,2,104000020,1 +1140,3,102000060,1 +1141,3,102000070,1 +1142,3,102000080,1 +1143,3,103000050,1 +1144,3,105000050,1 +1145,1,102000000,2 +1146,2,102000001,2 +1147,3,102000002,2 +1148,4,102000003,2 +1149,5,102000004,2 +1150,1,112000000,2 +1151,2,112000001,2 +1152,3,112000002,2 +1153,4,112000003,2 +1154,5,112000004,2 +1155,1,120020,3 +1156,1,120030,3 +1157,1,120040,3 +1158,1,120050,3 +1159,2,120021,3 +1160,2,120031,3 +1161,2,120041,3 +1162,2,120051,3 +1163,3,120240,3 +1164,3,120250,3 +1165,3,120260,3 +1166,3,120270,3 +1167,3,120300,3 +1168,3,120310,3 +1169,3,120320,3 +1170,3,120330,3 +1171,3,120340,3 +1172,3,120350,3 +1173,3,120360,3 +1174,3,120370,3 +1175,3,120380,3 +1176,3,120390,3 +1177,3,120400,3 +1178,3,120410,3 +1179,3,120420,3 +1180,3,120430,3 +1181,3,120450,3 +1182,3,120460,3 +1183,4,140000,3 +1185,2,104000030,1 +1186,2,104000040,1 +1187,2,108000020,1 +1188,3,104000060,1 +1189,3,104000070,1 +1190,3,104000080,1 +1191,3,102000050,1 +1192,4,104000100,1 +1193,4,104000110,1 +1194,4,107000090,1 +1195,1,111000000,2 +1196,2,111000001,2 +1197,3,111000002,2 +1198,4,111000003,2 +1199,5,111000004,2 +1200,1,120000000,2 +1201,2,120000001,2 +1202,3,120000002,2 +1203,4,120000003,2 +1204,5,120000004,2 +1205,3,170002,3 +1206,4,170003,3 +1208,2,104000030,1 +1209,2,104000040,1 +1210,2,108000020,1 +1211,3,104000060,1 +1212,3,104000070,1 +1213,3,104000080,1 +1214,3,102000050,1 +1215,4,104000100,1 +1216,4,104000110,1 +1217,4,107000090,1 +1218,1,111000000,2 +1219,2,111000001,2 +1220,3,111000002,2 +1221,4,111000003,2 +1222,5,111000004,2 +1223,1,120000000,2 +1224,2,120000001,2 +1225,3,120000002,2 +1226,4,120000003,2 +1227,5,120000004,2 +1228,1,110020,3 +1229,1,110030,3 +1230,1,110040,3 +1231,1,110050,3 +1232,2,110021,3 +1233,2,110031,3 +1234,2,110041,3 +1235,2,110051,3 +1236,3,110022,3 +1237,3,110032,3 +1238,3,110042,3 +1239,3,110052,3 +1240,3,110060,3 +1241,3,110070,3 +1242,3,110080,3 +1243,3,110090,3 +1244,3,110100,3 +1245,3,110110,3 +1246,3,110120,3 +1247,3,110130,3 +1248,3,110140,3 +1249,3,110150,3 +1250,3,110160,3 +1251,3,110170,3 +1252,3,110180,3 +1253,3,110190,3 +1254,3,110200,3 +1255,3,110210,3 +1256,3,110220,3 +1257,3,110230,3 +1258,3,110240,3 +1259,3,110250,3 +1260,3,110260,3 +1261,3,110270,3 +1262,4,140000,3 +1264,2,105000030,1 +1265,2,105000040,1 +1266,2,102000020,1 +1267,2,103000020,1 +1268,3,105000060,1 +1269,3,105000070,1 +1270,3,105000080,1 +1271,3,104000050,1 +1272,3,106000050,1 +1273,4,105000100,1 +1274,4,105000110,1 +1275,4,108000090,1 +1276,1,109000000,2 +1277,2,109000001,2 +1278,3,109000002,2 +1279,4,109000003,2 +1280,5,109000004,2 +1281,1,112000000,2 +1282,2,112000001,2 +1283,3,112000002,2 +1284,4,112000003,2 +1285,5,112000004,2 +1286,3,170002,3 +1287,4,170003,3 +1289,2,106000030,1 +1290,2,106000040,1 +1291,2,105000020,1 +1292,3,106000060,1 +1293,3,106000070,1 +1294,3,106000080,1 +1295,3,101000070,1 +1296,4,106000100,1 +1297,4,106000110,1 +1298,4,104000090,1 +1299,1,103000000,2 +1300,2,103000001,2 +1301,3,103000002,2 +1302,4,103000003,2 +1303,5,103000004,2 +1304,1,112000000,2 +1305,2,112000001,2 +1306,3,112000002,2 +1307,4,112000003,2 +1308,5,112000004,2 +1309,1,130020,3 +1310,1,130030,3 +1311,1,130040,3 +1312,1,130050,3 +1313,2,130021,3 +1314,2,130031,3 +1315,2,130041,3 +1316,2,130051,3 +1317,3,130022,3 +1318,3,130032,3 +1319,3,130042,3 +1320,3,130052,3 +1321,3,130060,3 +1322,3,130070,3 +1323,3,130080,3 +1324,3,130090,3 +1325,3,130100,3 +1326,3,130110,3 +1327,3,130120,3 +1328,3,130130,3 +1329,3,130140,3 +1330,3,130150,3 +1331,3,130160,3 +1332,3,130170,3 +1333,3,130180,3 +1334,3,130190,3 +1335,3,130200,3 +1336,3,130420,3 +1337,3,130510,3 +1338,3,130520,3 +1339,3,130530,3 +1340,3,130540,3 +1341,4,140000,3 +1343,2,108000030,1 +1344,2,108000040,1 +1345,2,106000020,1 +1346,3,108000060,1 +1347,3,108000070,1 +1348,3,108000080,1 +1349,3,107000050,1 +1350,4,108000100,1 +1351,4,105000090,1 +1352,1,108000000,2 +1353,2,108000001,2 +1354,3,108000002,2 +1355,4,108000003,2 +1356,5,108000004,2 +1357,1,120000000,2 +1358,2,120000001,2 +1359,3,120000002,2 +1360,4,120000003,2 +1361,5,120000004,2 +1362,1,120020,3 +1363,1,120030,3 +1364,1,120040,3 +1365,1,120050,3 +1366,2,120021,3 +1367,2,120031,3 +1368,2,120041,3 +1369,2,120051,3 +1370,3,120022,3 +1371,3,120032,3 +1372,3,120042,3 +1373,3,120052,3 +1374,3,120240,3 +1375,3,120250,3 +1376,3,120260,3 +1377,3,120270,3 +1378,3,120300,3 +1379,3,120310,3 +1380,3,120320,3 +1381,3,120330,3 +1382,3,120340,3 +1383,3,120350,3 +1384,3,120360,3 +1385,3,120370,3 +1386,3,120380,3 +1387,3,120390,3 +1388,3,120400,3 +1389,3,120410,3 +1390,3,120420,3 +1391,3,120430,3 +1392,3,120450,3 +1393,3,120460,3 +1394,4,140000,3 +1396,2,103000030,1 +1397,2,103000040,1 +1398,2,107000020,1 +1399,2,101000030,1 +1400,3,101000050,1 +1401,3,101000060,1 +1402,3,101000080,1 +1403,3,103000060,1 +1404,3,103000070,1 +1405,3,103000080,1 +1406,3,101000040,1 +1407,4,101000090,1 +1408,4,102000090,1 +1409,4,103000100,1 +1410,4,101000100,1 +1411,4,101000110,1 +1412,1,101000000,2 +1413,2,101000001,2 +1414,3,101000002,2 +1415,4,101000008,2 +1416,5,101000004,2 +1417,1,120000000,2 +1418,2,120000001,2 +1419,3,120000002,2 +1420,4,120000003,2 +1421,5,120000004,2 +1422,3,170002,3 +1423,4,170003,3 +1425,2,105000030,1 +1426,2,105000040,1 +1427,2,102000020,1 +1428,2,103000020,1 +1429,3,105000060,1 +1430,3,105000070,1 +1431,3,105000080,1 +1432,3,104000050,1 +1433,3,106000050,1 +1434,4,105000100,1 +1435,4,105000110,1 +1436,4,108000090,1 +1437,1,109000000,2 +1438,2,109000001,2 +1439,3,109000002,2 +1440,4,109000003,2 +1441,5,109000004,2 +1442,1,112000000,2 +1443,2,112000001,2 +1444,3,112000002,2 +1445,4,112000003,2 +1446,5,112000004,2 +1447,3,180002,3 +1448,4,180003,3 +1450,2,107000030,1 +1451,2,107000040,1 +1452,2,101000020,1 +1453,3,107000060,1 +1454,3,107000070,1 +1455,3,107000080,1 +1456,3,108000050,1 +1457,4,107000100,1 +1458,4,103000090,1 +1459,1,105000000,2 +1460,2,105000001,2 +1461,3,105000002,2 +1462,4,105000003,2 +1463,5,105000004,2 +1464,1,120000000,2 +1465,2,120000001,2 +1466,3,120000002,2 +1467,4,120000003,2 +1468,5,120000004,2 +1469,3,170002,3 +1470,4,170003,3 +1472,2,104000030,1 +1473,2,104000040,1 +1474,2,108000020,1 +1475,3,104000060,1 +1476,3,104000070,1 +1477,3,104000080,1 +1478,3,102000050,1 +1479,4,104000100,1 +1480,4,104000110,1 +1481,4,107000090,1 +1482,1,111000000,2 +1483,2,111000001,2 +1484,3,111000002,2 +1485,4,111000003,2 +1486,5,111000004,2 +1487,1,120000000,2 +1488,2,120000001,2 +1489,3,120000002,2 +1490,4,120000003,2 +1491,5,120000004,2 +1492,1,110020,3 +1493,1,110030,3 +1494,1,110040,3 +1495,1,110050,3 +1496,2,110021,3 +1497,2,110031,3 +1498,2,110041,3 +1499,2,110051,3 +1500,3,110022,3 +1501,3,110032,3 +1502,3,110042,3 +1503,3,110052,3 +1504,3,110060,3 +1505,3,110070,3 +1506,3,110080,3 +1507,3,110090,3 +1508,3,110100,3 +1509,3,110110,3 +1510,3,110120,3 +1511,3,110130,3 +1512,3,110140,3 +1513,3,110150,3 +1514,3,110160,3 +1515,3,110170,3 +1516,3,110180,3 +1517,3,110190,3 +1518,3,110200,3 +1519,3,110210,3 +1520,3,110220,3 +1521,3,110230,3 +1522,3,110240,3 +1523,3,110250,3 +1524,3,110260,3 +1525,3,110270,3 +1526,4,140000,3 +1528,2,108000030,1 +1529,2,108000040,1 +1530,2,106000020,1 +1531,3,108000060,1 +1532,3,108000070,1 +1533,3,108000080,1 +1534,3,107000050,1 +1535,4,108000100,1 +1536,4,105000090,1 +1537,1,108000000,2 +1538,2,108000001,2 +1539,3,108000002,2 +1540,4,108000003,2 +1541,5,108000004,2 +1542,1,120000000,2 +1543,2,120000001,2 +1544,3,120000002,2 +1545,4,120000003,2 +1546,5,120000004,2 +1547,3,170002,3 +1548,4,170003,3 +1550,2,103000030,1 +1551,2,103000040,1 +1552,2,107000020,1 +1553,2,101000030,1 +1554,3,101000050,1 +1555,3,101000060,1 +1556,3,101000080,1 +1557,3,103000060,1 +1558,3,103000070,1 +1559,3,103000080,1 +1560,3,101000040,1 +1561,4,101000090,1 +1562,4,102000090,1 +1563,4,103000100,1 +1564,4,101000100,1 +1565,4,101000110,1 +1566,1,101000000,2 +1567,2,101000001,2 +1568,3,101000002,2 +1569,4,101000008,2 +1570,5,101000004,2 +1571,1,112000000,2 +1572,2,112000001,2 +1573,3,112000002,2 +1574,4,112000003,2 +1575,5,112000004,2 +1576,1,120020,3 +1577,1,120030,3 +1578,1,120040,3 +1579,1,120050,3 +1580,2,120021,3 +1581,2,120031,3 +1582,2,120041,3 +1583,2,120051,3 +1584,3,120022,3 +1585,3,120032,3 +1586,3,120042,3 +1587,3,120052,3 +1588,4,120023,3 +1589,4,120033,3 +1590,4,120043,3 +1591,4,120053,3 +1592,3,120240,3 +1593,3,120250,3 +1594,3,120260,3 +1595,3,120270,3 +1596,3,120300,3 +1597,3,120310,3 +1598,3,120320,3 +1599,3,120330,3 +1600,3,120340,3 +1601,3,120350,3 +1602,3,120360,3 +1603,3,120370,3 +1604,3,120380,3 +1605,3,120390,3 +1606,3,120400,3 +1607,3,120410,3 +1608,3,120420,3 +1609,3,120430,3 +1610,3,120450,3 +1611,3,120460,3 +1612,4,140000,3 +1613,4,150010,3 +1614,4,150020,3 +1615,4,150030,3 +1616,4,150040,3 +1618,2,102000030,1 +1619,2,102000040,1 +1620,2,104000020,1 +1621,3,102000060,1 +1622,3,102000070,1 +1623,3,102000080,1 +1624,3,103000050,1 +1625,3,105000050,1 +1626,4,102000100,1 +1627,4,102000110,1 +1628,4,106000090,1 +1629,1,102000000,2 +1630,2,102000001,2 +1631,3,102000002,2 +1632,4,102000003,2 +1633,5,102000004,2 +1634,1,112000000,2 +1635,2,112000001,2 +1636,3,112000002,2 +1637,4,112000003,2 +1638,5,112000004,2 +1639,1,110020,3 +1640,1,110030,3 +1641,1,110040,3 +1642,1,110050,3 +1643,2,110021,3 +1644,2,110031,3 +1645,2,110041,3 +1646,2,110051,3 +1647,3,110022,3 +1648,3,110032,3 +1649,3,110042,3 +1650,3,110052,3 +1651,4,110023,3 +1652,4,110033,3 +1653,4,110043,3 +1654,4,110053,3 +1655,3,110060,3 +1656,3,110070,3 +1657,3,110080,3 +1658,3,110090,3 +1659,3,110100,3 +1660,3,110110,3 +1661,3,110120,3 +1662,3,110130,3 +1663,3,110140,3 +1664,3,110150,3 +1665,3,110160,3 +1666,3,110170,3 +1667,3,110180,3 +1668,3,110190,3 +1669,3,110200,3 +1670,3,110210,3 +1671,3,110220,3 +1672,3,110230,3 +1673,3,110240,3 +1674,3,110250,3 +1675,3,110260,3 +1676,3,110270,3 +1677,4,140000,3 +1678,4,150010,3 +1679,4,150020,3 +1680,4,150030,3 +1681,4,150040,3 +1683,2,106000030,1 +1684,2,106000040,1 +1685,2,105000020,1 +1686,3,106000060,1 +1687,3,106000070,1 +1688,3,106000080,1 +1689,3,101000070,1 +1690,4,106000100,1 +1691,4,106000110,1 +1692,4,104000090,1 +1693,1,103000000,2 +1694,2,103000001,2 +1695,3,103000002,2 +1696,4,103000003,2 +1697,5,103000004,2 +1698,1,112000000,2 +1699,2,112000001,2 +1700,3,112000002,2 +1701,4,112000003,2 +1702,5,112000004,2 +1703,1,120020,3 +1704,1,120030,3 +1705,1,120040,3 +1706,1,120050,3 +1707,2,120021,3 +1708,2,120031,3 +1709,2,120041,3 +1710,2,120051,3 +1711,3,120022,3 +1712,3,120032,3 +1713,3,120042,3 +1714,3,120052,3 +1715,4,120023,3 +1716,4,120033,3 +1717,4,120043,3 +1718,4,120053,3 +1719,3,120240,3 +1720,3,120250,3 +1721,3,120260,3 +1722,3,120270,3 +1723,3,120300,3 +1724,3,120310,3 +1725,3,120320,3 +1726,3,120330,3 +1727,3,120340,3 +1728,3,120350,3 +1729,3,120360,3 +1730,3,120370,3 +1731,3,120380,3 +1732,3,120390,3 +1733,3,120400,3 +1734,3,120410,3 +1735,3,120420,3 +1736,3,120430,3 +1737,3,120450,3 +1738,3,120460,3 +1739,4,140000,3 +1740,4,150010,3 +1741,4,150020,3 +1742,4,150030,3 +1743,4,150040,3 +1745,2,103000030,1 +1746,2,103000040,1 +1747,2,107000020,1 +1748,2,101000030,1 +1749,3,101000050,1 +1750,3,101000060,1 +1751,3,101000080,1 +1752,3,103000060,1 +1753,3,103000070,1 +1754,3,103000080,1 +1755,3,101000040,1 +1756,4,101000090,1 +1757,4,102000090,1 +1758,4,103000100,1 +1759,4,101000100,1 +1760,4,101000110,1 +1761,1,101000000,2 +1762,2,101000001,2 +1763,3,101000002,2 +1764,4,101000008,2 +1765,5,101000004,2 +1766,1,120000000,2 +1767,2,120000001,2 +1768,3,120000002,2 +1769,4,120000003,2 +1770,5,120000004,2 +1771,3,170003,3 +1772,4,170004,3 +1774,2,107000030,1 +1775,2,107000040,1 +1776,2,101000020,1 +1777,3,107000060,1 +1778,3,107000070,1 +1779,3,107000080,1 +1780,3,108000050,1 +1781,4,107000100,1 +1782,4,103000090,1 +1783,1,105000000,2 +1784,2,105000001,2 +1785,3,105000002,2 +1786,4,105000003,2 +1787,5,105000004,2 +1788,1,120000000,2 +1789,2,120000001,2 +1790,3,120000002,2 +1791,4,120000003,2 +1792,5,120000004,2 +1793,1,130020,3 +1794,1,130030,3 +1795,1,130040,3 +1796,1,130050,3 +1797,2,130021,3 +1798,2,130031,3 +1799,2,130041,3 +1800,2,130051,3 +1801,3,130022,3 +1802,3,130032,3 +1803,3,130042,3 +1804,3,130052,3 +1805,4,130023,3 +1806,4,130033,3 +1807,4,130043,3 +1808,4,130053,3 +1809,3,130060,3 +1810,3,130070,3 +1811,3,130080,3 +1812,3,130090,3 +1813,3,130100,3 +1814,3,130110,3 +1815,3,130120,3 +1816,3,130130,3 +1817,3,130140,3 +1818,3,130150,3 +1819,3,130160,3 +1820,3,130170,3 +1821,3,130180,3 +1822,3,130190,3 +1823,3,130200,3 +1824,3,130420,3 +1825,3,130510,3 +1826,3,130520,3 +1827,3,130530,3 +1828,3,130540,3 +1829,4,140000,3 +1830,4,150010,3 +1831,4,150020,3 +1832,4,150030,3 +1833,4,150040,3 +1835,2,102000030,1 +1836,2,102000040,1 +1837,2,104000020,1 +1838,3,102000060,1 +1839,3,102000070,1 +1840,3,102000080,1 +1841,3,103000050,1 +1842,3,105000050,1 +1843,4,102000100,1 +1844,4,102000110,1 +1845,4,106000090,1 +1846,1,102000000,2 +1847,2,102000001,2 +1848,3,102000002,2 +1849,4,102000003,2 +1850,5,102000004,2 +1851,1,112000000,2 +1852,2,112000001,2 +1853,3,112000002,2 +1854,4,112000003,2 +1855,5,112000004,2 +1856,1,120020,3 +1857,1,120030,3 +1858,1,120040,3 +1859,1,120050,3 +1860,2,120021,3 +1861,2,120031,3 +1862,2,120041,3 +1863,2,120051,3 +1864,3,120022,3 +1865,3,120032,3 +1866,3,120042,3 +1867,3,120052,3 +1868,4,120023,3 +1869,4,120033,3 +1870,4,120043,3 +1871,4,120053,3 +1872,3,120240,3 +1873,3,120250,3 +1874,3,120260,3 +1875,3,120270,3 +1876,3,120300,3 +1877,3,120310,3 +1878,3,120320,3 +1879,3,120330,3 +1880,3,120340,3 +1881,3,120350,3 +1882,3,120360,3 +1883,3,120370,3 +1884,3,120380,3 +1885,3,120390,3 +1886,3,120400,3 +1887,3,120410,3 +1888,3,120420,3 +1889,3,120430,3 +1890,3,120450,3 +1891,3,120460,3 +1892,4,140000,3 +1893,4,150010,3 +1894,4,150020,3 +1895,4,150030,3 +1896,4,150040,3 +1898,2,108000030,1 +1899,2,108000040,1 +1900,2,106000020,1 +1901,3,108000060,1 +1902,3,108000070,1 +1903,3,108000080,1 +1904,3,107000050,1 +1905,4,108000100,1 +1906,4,105000090,1 +1907,1,108000000,2 +1908,2,108000001,2 +1909,3,108000002,2 +1910,4,108000003,2 +1911,5,108000004,2 +1912,1,120000000,2 +1913,2,120000001,2 +1914,3,120000002,2 +1915,4,120000003,2 +1916,5,120000004,2 +1917,3,170003,3 +1918,4,170004,3 +1920,2,103000030,1 +1921,2,103000040,1 +1922,2,107000020,1 +1923,2,101000030,1 +1924,3,101000050,1 +1925,3,101000060,1 +1926,3,101000080,1 +1927,3,103000060,1 +1928,3,103000070,1 +1929,3,103000080,1 +1930,3,101000040,1 +1931,4,101000090,1 +1932,4,102000090,1 +1933,4,103000100,1 +1934,4,101000100,1 +1935,4,101000110,1 +1936,1,101000000,2 +1937,2,101000001,2 +1938,3,101000002,2 +1939,4,101000008,2 +1940,5,101000004,2 +1941,1,112000000,2 +1942,2,112000001,2 +1943,3,112000002,2 +1944,4,112000003,2 +1945,5,112000004,2 +1946,3,170003,3 +1947,4,170004,3 +1949,2,105000030,1 +1950,2,105000040,1 +1951,2,102000020,1 +1952,2,103000020,1 +1953,3,105000060,1 +1954,3,105000070,1 +1955,3,105000080,1 +1956,3,104000050,1 +1957,3,106000050,1 +1958,4,105000100,1 +1959,4,105000110,1 +1960,4,108000090,1 +1961,1,109000000,2 +1962,2,109000001,2 +1963,3,109000002,2 +1964,4,109000003,2 +1965,5,109000004,2 +1966,1,112000000,2 +1967,2,112000001,2 +1968,3,112000002,2 +1969,4,112000003,2 +1970,5,112000004,2 +1971,3,180003,3 +1972,4,180004,3 +1974,2,104000030,1 +1975,2,104000040,1 +1976,2,108000020,1 +1977,3,104000060,1 +1978,3,104000070,1 +1979,3,104000080,1 +1980,3,102000050,1 +1981,4,104000100,1 +1982,4,104000110,1 +1983,4,107000090,1 +1984,1,111000000,2 +1985,2,111000001,2 +1986,3,111000002,2 +1987,4,111000003,2 +1988,5,111000004,2 +1989,1,120000000,2 +1990,2,120000001,2 +1991,3,120000002,2 +1992,4,120000003,2 +1993,5,120000004,2 +1994,1,110020,3 +1995,1,110030,3 +1996,1,110040,3 +1997,1,110050,3 +1998,2,110021,3 +1999,2,110031,3 +2000,2,110041,3 +2001,2,110051,3 +2002,3,110022,3 +2003,3,110032,3 +2004,3,110042,3 +2005,3,110052,3 +2006,4,110023,3 +2007,4,110033,3 +2008,4,110043,3 +2009,4,110053,3 +2010,3,110060,3 +2011,3,110070,3 +2012,3,110080,3 +2013,3,110090,3 +2014,3,110100,3 +2015,3,110110,3 +2016,3,110120,3 +2017,3,110130,3 +2018,3,110140,3 +2019,3,110150,3 +2020,3,110160,3 +2021,3,110170,3 +2022,3,110180,3 +2023,3,110190,3 +2024,3,110200,3 +2025,3,110210,3 +2026,3,110220,3 +2027,3,110230,3 +2028,3,110240,3 +2029,3,110250,3 +2030,3,110260,3 +2031,3,110270,3 +2032,4,140000,3 +2033,4,150010,3 +2034,4,150020,3 +2035,4,150030,3 +2036,4,150040,3 +2038,3,105000060,1 +2039,3,105000070,1 +2040,3,105000080,1 +2041,3,104000050,1 +2042,3,106000050,1 +2043,4,105000100,1 +2044,4,105000110,1 +2045,4,108000090,1 +2046,5,105000120,1 +2047,1,109000000,2 +2048,2,109000001,2 +2049,3,109000002,2 +2050,4,109000003,2 +2051,5,109000004,2 +2052,1,112000000,2 +2053,2,112000001,2 +2054,3,112000002,2 +2055,4,112000003,2 +2056,5,112000004,2 +2057,3,170004,3 +2059,3,101000050,1 +2060,3,101000060,1 +2061,3,101000080,1 +2062,3,103000060,1 +2063,3,103000070,1 +2064,3,103000080,1 +2065,3,101000040,1 +2066,4,101000090,1 +2067,4,102000090,1 +2068,4,103000100,1 +2069,4,101000100,1 +2070,4,101000110,1 +2071,5,101000120,1 +2072,5,103000120,1 +2073,1,101000000,2 +2074,2,101000001,2 +2075,3,101000002,2 +2076,4,101000008,2 +2077,5,101000004,2 +2078,1,120000000,2 +2079,2,120000001,2 +2080,3,120000002,2 +2081,4,120000003,2 +2082,5,120000004,2 +2083,3,170004,3 +2085,3,107000060,1 +2086,3,107000070,1 +2087,3,107000080,1 +2088,3,108000050,1 +2089,4,107000100,1 +2090,4,103000090,1 +2091,5,107000110,1 +2092,1,105000000,2 +2093,2,105000001,2 +2094,3,105000002,2 +2095,4,105000003,2 +2096,5,105000004,2 +2097,1,120000000,2 +2098,2,120000001,2 +2099,3,120000002,2 +2100,4,120000003,2 +2101,5,120000004,2 +2102,3,170004,3 +2104,3,101000050,1 +2105,3,101000060,1 +2106,3,101000080,1 +2107,3,103000060,1 +2108,3,103000070,1 +2109,3,103000080,1 +2110,3,101000040,1 +2111,4,101000090,1 +2112,4,102000090,1 +2113,4,103000100,1 +2114,4,101000100,1 +2115,4,101000110,1 +2116,5,101000120,1 +2117,5,103000120,1 +2118,1,101000000,2 +2119,2,101000001,2 +2120,3,101000002,2 +2121,4,101000008,2 +2122,5,101000004,2 +2123,1,112000000,2 +2124,2,112000001,2 +2125,3,112000002,2 +2126,4,112000003,2 +2127,5,112000004,2 +2128,1,130020,3 +2129,1,130030,3 +2130,1,130040,3 +2131,1,130050,3 +2132,2,130021,3 +2133,2,130031,3 +2134,2,130041,3 +2135,2,130051,3 +2136,3,130022,3 +2137,3,130032,3 +2138,3,130042,3 +2139,3,130052,3 +2140,4,130023,3 +2141,4,130033,3 +2142,4,130043,3 +2143,4,130053,3 +2144,5,130024,3 +2145,5,130034,3 +2146,5,130044,3 +2147,5,130054,3 +2148,3,130060,3 +2149,3,130070,3 +2150,3,130080,3 +2151,3,130090,3 +2152,3,130100,3 +2153,3,130110,3 +2154,3,130120,3 +2155,3,130130,3 +2156,3,130140,3 +2157,3,130150,3 +2158,3,130160,3 +2159,3,130170,3 +2160,3,130180,3 +2161,3,130190,3 +2162,3,130200,3 +2163,3,130420,3 +2164,3,130510,3 +2165,3,130520,3 +2166,3,130530,3 +2167,3,130540,3 +2168,4,140000,3 +2169,4,150010,3 +2170,4,150020,3 +2171,4,150030,3 +2172,4,150040,3 +2174,3,102000060,1 +2175,3,102000070,1 +2176,3,102000080,1 +2177,3,103000050,1 +2178,3,105000050,1 +2179,4,102000100,1 +2180,4,102000110,1 +2181,4,106000090,1 +2182,5,102000120,1 +2183,1,102000000,2 +2184,2,102000001,2 +2185,3,102000002,2 +2186,4,102000003,2 +2187,5,102000004,2 +2188,1,112000000,2 +2189,2,112000001,2 +2190,3,112000002,2 +2191,4,112000003,2 +2192,5,112000004,2 +2193,3,170004,3 +2195,3,101000050,1 +2196,3,101000060,1 +2197,3,101000080,1 +2198,3,103000060,1 +2199,3,103000070,1 +2200,3,103000080,1 +2201,3,101000040,1 +2202,4,101000090,1 +2203,4,102000090,1 +2204,4,103000100,1 +2205,4,101000100,1 +2206,4,101000110,1 +2207,5,101000120,1 +2208,5,103000120,1 +2209,1,101000000,2 +2210,2,101000001,2 +2211,3,101000002,2 +2212,4,101000008,2 +2213,5,101000004,2 +2214,1,120000000,2 +2215,2,120000001,2 +2216,3,120000002,2 +2217,4,120000003,2 +2218,5,120000004,2 +2219,3,170004,3 +2221,3,106000060,1 +2222,3,106000070,1 +2223,3,106000080,1 +2224,3,101000070,1 +2225,4,106000100,1 +2226,4,106000110,1 +2227,4,104000090,1 +2228,5,106000120,1 +2229,1,103000000,2 +2230,2,103000001,2 +2231,3,103000002,2 +2232,4,103000003,2 +2233,5,103000004,2 +2234,1,112000000,2 +2235,2,112000001,2 +2236,3,112000002,2 +2237,4,112000003,2 +2238,5,112000004,2 +2239,1,130020,3 +2240,1,130030,3 +2241,1,130040,3 +2242,1,130050,3 +2243,2,130021,3 +2244,2,130031,3 +2245,2,130041,3 +2246,2,130051,3 +2247,3,130022,3 +2248,3,130032,3 +2249,3,130042,3 +2250,3,130052,3 +2251,4,130023,3 +2252,4,130033,3 +2253,4,130043,3 +2254,4,130053,3 +2255,5,130024,3 +2256,5,130034,3 +2257,5,130044,3 +2258,5,130054,3 +2259,3,130060,3 +2260,3,130070,3 +2261,3,130080,3 +2262,3,130090,3 +2263,3,130100,3 +2264,3,130110,3 +2265,3,130120,3 +2266,3,130130,3 +2267,3,130140,3 +2268,3,130150,3 +2269,3,130160,3 +2270,3,130170,3 +2271,3,130180,3 +2272,3,130190,3 +2273,3,130200,3 +2274,3,130420,3 +2275,3,130510,3 +2276,3,130520,3 +2277,3,130530,3 +2278,3,130540,3 +2279,4,140000,3 +2280,4,150010,3 +2281,4,150020,3 +2282,4,150030,3 +2283,4,150040,3 +2285,3,108000060,1 +2286,3,108000070,1 +2287,3,108000080,1 +2288,3,107000050,1 +2289,4,108000100,1 +2290,4,105000090,1 +2291,5,108000110,1 +2292,1,108000000,2 +2293,2,108000001,2 +2294,3,108000002,2 +2295,4,108000003,2 +2296,5,108000004,2 +2297,1,120000000,2 +2298,2,120000001,2 +2299,3,120000002,2 +2300,4,120000003,2 +2301,5,120000004,2 +2302,1,120020,3 +2303,1,120030,3 +2304,1,120040,3 +2305,1,120050,3 +2306,2,120021,3 +2307,2,120031,3 +2308,2,120041,3 +2309,2,120051,3 +2310,3,120022,3 +2311,3,120032,3 +2312,3,120042,3 +2313,3,120052,3 +2314,4,120023,3 +2315,4,120033,3 +2316,4,120043,3 +2317,4,120053,3 +2318,5,120024,3 +2319,5,120034,3 +2320,5,120044,3 +2321,5,120054,3 +2322,3,120240,3 +2323,3,120250,3 +2324,3,120260,3 +2325,3,120270,3 +2326,3,120300,3 +2327,3,120310,3 +2328,3,120320,3 +2329,3,120330,3 +2330,3,120340,3 +2331,3,120350,3 +2332,3,120360,3 +2333,3,120370,3 +2334,3,120380,3 +2335,3,120390,3 +2336,3,120400,3 +2337,3,120410,3 +2338,3,120420,3 +2339,3,120430,3 +2340,3,120450,3 +2341,3,120460,3 +2342,4,140000,3 +2343,4,150010,3 +2344,4,150020,3 +2345,4,150030,3 +2346,4,150040,3 +2348,3,104000060,1 +2349,3,104000070,1 +2350,3,104000080,1 +2351,3,102000050,1 +2352,4,104000100,1 +2353,4,104000110,1 +2354,4,107000090,1 +2355,5,104000120,1 +2356,1,111000000,2 +2357,2,111000001,2 +2358,3,111000002,2 +2359,4,111000003,2 +2360,5,111000004,2 +2361,1,120000000,2 +2362,2,120000001,2 +2363,3,120000002,2 +2364,4,120000003,2 +2365,5,120000004,2 +2366,1,110020,3 +2367,1,110030,3 +2368,1,110040,3 +2369,1,110050,3 +2370,2,110021,3 +2371,2,110031,3 +2372,2,110041,3 +2373,2,110051,3 +2374,3,110022,3 +2375,3,110032,3 +2376,3,110042,3 +2377,3,110052,3 +2378,4,110023,3 +2379,4,110033,3 +2380,4,110043,3 +2381,4,110053,3 +2382,5,110024,3 +2383,5,110034,3 +2384,5,110044,3 +2385,5,110054,3 +2386,3,110060,3 +2387,3,110070,3 +2388,3,110080,3 +2389,3,110090,3 +2390,3,110100,3 +2391,3,110110,3 +2392,3,110120,3 +2393,3,110130,3 +2394,3,110140,3 +2395,3,110150,3 +2396,3,110160,3 +2397,3,110170,3 +2398,3,110180,3 +2399,3,110190,3 +2400,3,110200,3 +2401,3,110210,3 +2402,3,110220,3 +2403,3,110230,3 +2404,3,110240,3 +2405,3,110250,3 +2406,3,110260,3 +2407,3,110270,3 +2408,4,140000,3 +2409,4,150010,3 +2410,4,150020,3 +2411,4,150030,3 +2412,4,150040,3 +2414,3,105000060,1 +2415,3,105000070,1 +2416,3,105000080,1 +2417,3,104000050,1 +2418,3,106000050,1 +2419,4,105000100,1 +2420,4,105000110,1 +2421,4,108000090,1 +2422,5,105000120,1 +2423,1,109000000,2 +2424,2,109000001,2 +2425,3,109000002,2 +2426,4,109000003,2 +2427,5,109000004,2 +2428,1,112000000,2 +2429,2,112000001,2 +2430,3,112000002,2 +2431,4,112000003,2 +2432,5,112000004,2 +2433,3,170004,3 +2435,3,104000060,1 +2436,3,104000070,1 +2437,3,104000080,1 +2438,3,102000050,1 +2439,4,104000100,1 +2440,4,104000110,1 +2441,4,107000090,1 +2442,5,104000120,1 +2443,1,111000000,2 +2444,2,111000001,2 +2445,3,111000002,2 +2446,4,111000003,2 +2447,5,111000004,2 +2448,1,120000000,2 +2449,2,120000001,2 +2450,3,120000002,2 +2451,4,120000003,2 +2452,5,120000004,2 +2453,1,130020,3 +2454,1,130030,3 +2455,1,130040,3 +2456,1,130050,3 +2457,2,130021,3 +2458,2,130031,3 +2459,2,130041,3 +2460,2,130051,3 +2461,3,130022,3 +2462,3,130032,3 +2463,3,130042,3 +2464,3,130052,3 +2465,4,130023,3 +2466,4,130033,3 +2467,4,130043,3 +2468,4,130053,3 +2469,5,130024,3 +2470,5,130034,3 +2471,5,130044,3 +2472,5,130054,3 +2473,3,130060,3 +2474,3,130070,3 +2475,3,130080,3 +2476,3,130090,3 +2477,3,130100,3 +2478,3,130110,3 +2479,3,130120,3 +2480,3,130130,3 +2481,3,130140,3 +2482,3,130150,3 +2483,3,130160,3 +2484,3,130170,3 +2485,3,130180,3 +2486,3,130190,3 +2487,3,130200,3 +2488,3,130420,3 +2489,3,130510,3 +2490,3,130520,3 +2491,3,130530,3 +2492,3,130540,3 +2493,4,140000,3 +2494,4,150010,3 +2495,4,150020,3 +2496,4,150030,3 +2497,4,150040,3 +2500,1,101000000,2 +2501,2,101000001,2 +2502,3,101000002,2 +2503,4,101000008,2 +2504,5,101000004,2 +2505,1,102000000,2 +2506,2,102000001,2 +2507,3,102000002,2 +2508,4,102000003,2 +2509,5,102000004,2 +2510,1,103000000,2 +2511,2,103000001,2 +2512,3,103000002,2 +2513,4,103000003,2 +2514,5,103000004,2 +2515,1,105000000,2 +2516,2,105000001,2 +2517,3,105000002,2 +2518,4,105000003,2 +2519,5,105000004,2 +2520,1,108000000,2 +2521,2,108000001,2 +2522,3,108000002,2 +2523,4,108000003,2 +2524,5,108000004,2 +2525,1,109000000,2 +2526,2,109000001,2 +2527,3,109000002,2 +2528,4,109000003,2 +2529,5,109000004,2 +2530,1,111000000,2 +2531,2,111000001,2 +2532,3,111000002,2 +2533,4,111000003,2 +2534,5,111000004,2 +2535,1,112000000,2 +2536,2,112000001,2 +2537,3,112000002,2 +2538,4,112000003,2 +2539,5,112000004,2 +2540,1,120000000,2 +2541,2,120000001,2 +2542,3,120000002,2 +2543,4,120000003,2 +2544,5,120000004,2 +2545,1,101000010,1 +2546,2,101000020,1 +2547,2,101000030,1 +2548,3,101000040,1 +2549,3,101000050,1 +2550,3,101000060,1 +2551,3,101000070,1 +2552,3,101000080,1 +2553,1,102000010,1 +2554,2,102000020,1 +2555,2,102000030,1 +2556,2,102000040,1 +2557,3,102000050,1 +2558,3,102000060,1 +2559,3,102000070,1 +2560,3,102000080,1 +2561,1,103000010,1 +2562,2,103000020,1 +2563,2,103000030,1 +2564,2,103000040,1 +2565,3,103000050,1 +2566,3,103000060,1 +2567,3,103000070,1 +2568,3,103000080,1 +2569,1,104000010,1 +2570,2,104000020,1 +2571,2,104000030,1 +2572,2,104000040,1 +2573,3,104000050,1 +2574,3,104000060,1 +2575,3,104000070,1 +2576,3,104000080,1 +2577,1,105000010,1 +2578,2,105000020,1 +2579,2,105000030,1 +2580,2,105000040,1 +2581,3,105000050,1 +2582,3,105000060,1 +2583,3,105000070,1 +2584,3,105000080,1 +2585,1,106000010,1 +2586,2,106000020,1 +2587,2,106000030,1 +2588,2,106000040,1 +2589,3,106000050,1 +2590,3,106000060,1 +2591,3,106000070,1 +2592,3,106000080,1 +2593,1,107000010,1 +2594,2,107000020,1 +2595,2,107000030,1 +2596,2,107000040,1 +2597,3,107000050,1 +2598,3,107000060,1 +2599,3,107000070,1 +2600,3,107000080,1 +2601,1,108000010,1 +2602,2,108000020,1 +2603,2,108000030,1 +2604,2,108000040,1 +2605,3,108000050,1 +2606,3,108000060,1 +2607,3,108000070,1 +2608,3,108000080,1 +2609,2,180001,3 +2611,1,101000000,2 +2612,2,101000001,2 +2613,3,101000002,2 +2614,4,101000008,2 +2615,5,101000004,2 +2616,1,102000000,2 +2617,2,102000001,2 +2618,3,102000002,2 +2619,4,102000003,2 +2620,5,102000004,2 +2621,1,103000000,2 +2622,2,103000001,2 +2623,3,103000002,2 +2624,4,103000003,2 +2625,5,103000004,2 +2626,1,105000000,2 +2627,2,105000001,2 +2628,3,105000002,2 +2629,4,105000003,2 +2630,5,105000004,2 +2631,1,108000000,2 +2632,2,108000001,2 +2633,3,108000002,2 +2634,4,108000003,2 +2635,5,108000004,2 +2636,1,109000000,2 +2637,2,109000001,2 +2638,3,109000002,2 +2639,4,109000003,2 +2640,5,109000004,2 +2641,1,111000000,2 +2642,2,111000001,2 +2643,3,111000002,2 +2644,4,111000003,2 +2645,5,111000004,2 +2646,1,112000000,2 +2647,2,112000001,2 +2648,3,112000002,2 +2649,4,112000003,2 +2650,5,112000004,2 +2651,1,120000000,2 +2652,2,120000001,2 +2653,3,120000002,2 +2654,4,120000003,2 +2655,5,120000004,2 +2656,1,101000010,1 +2657,2,101000020,1 +2658,2,101000030,1 +2659,3,101000040,1 +2660,3,101000050,1 +2661,3,101000060,1 +2662,3,101000070,1 +2663,3,101000080,1 +2664,1,102000010,1 +2665,2,102000020,1 +2666,2,102000030,1 +2667,2,102000040,1 +2668,3,102000050,1 +2669,3,102000060,1 +2670,3,102000070,1 +2671,3,102000080,1 +2672,1,103000010,1 +2673,2,103000020,1 +2674,2,103000030,1 +2675,2,103000040,1 +2676,3,103000050,1 +2677,3,103000060,1 +2678,3,103000070,1 +2679,3,103000080,1 +2680,1,104000010,1 +2681,2,104000020,1 +2682,2,104000030,1 +2683,2,104000040,1 +2684,3,104000050,1 +2685,3,104000060,1 +2686,3,104000070,1 +2687,3,104000080,1 +2688,1,105000010,1 +2689,2,105000020,1 +2690,2,105000030,1 +2691,2,105000040,1 +2692,3,105000050,1 +2693,3,105000060,1 +2694,3,105000070,1 +2695,3,105000080,1 +2696,1,106000010,1 +2697,2,106000020,1 +2698,2,106000030,1 +2699,2,106000040,1 +2700,3,106000050,1 +2701,3,106000060,1 +2702,3,106000070,1 +2703,3,106000080,1 +2704,1,107000010,1 +2705,2,107000020,1 +2706,2,107000030,1 +2707,2,107000040,1 +2708,3,107000050,1 +2709,3,107000060,1 +2710,3,107000070,1 +2711,3,107000080,1 +2712,1,108000010,1 +2713,2,108000020,1 +2714,2,108000030,1 +2715,2,108000040,1 +2716,3,108000050,1 +2717,3,108000060,1 +2718,3,108000070,1 +2719,3,108000080,1 +2720,1,109000010,1 +2721,2,109000020,1 +2722,2,109000030,1 +2723,2,109000040,1 +2724,3,109000050,1 +2725,3,109000060,1 +2726,3,109000070,1 +2727,3,109000080,1 +2728,2,180001,3 +2731,1,101000000,2 +2732,2,101000001,2 +2733,3,101000002,2 +2734,4,101000008,2 +2735,5,101000004,2 +2736,1,102000000,2 +2737,2,102000001,2 +2738,3,102000002,2 +2739,4,102000003,2 +2740,5,102000004,2 +2741,1,103000000,2 +2742,2,103000001,2 +2743,3,103000002,2 +2744,4,103000003,2 +2745,5,103000004,2 +2746,1,105000000,2 +2747,2,105000001,2 +2748,3,105000002,2 +2749,4,105000003,2 +2750,5,105000004,2 +2751,1,107000000,2 +2752,2,107000001,2 +2753,3,107000002,2 +2754,4,107000003,2 +2755,5,107000004,2 +2756,1,108000000,2 +2757,2,108000001,2 +2758,3,108000002,2 +2759,4,108000003,2 +2760,5,108000004,2 +2761,1,109000000,2 +2762,2,109000001,2 +2763,3,109000002,2 +2764,4,109000003,2 +2765,5,109000004,2 +2766,1,111000000,2 +2767,2,111000001,2 +2768,3,111000002,2 +2769,4,111000003,2 +2770,5,111000004,2 +2771,1,112000000,2 +2772,2,112000001,2 +2773,3,112000002,2 +2774,4,112000003,2 +2775,5,112000004,2 +2776,1,120000000,2 +2777,2,120000001,2 +2778,3,120000002,2 +2779,4,120000003,2 +2780,5,120000004,2 +2781,1,101000010,1 +2782,2,101000020,1 +2783,2,101000030,1 +2784,3,101000040,1 +2785,3,101000050,1 +2786,3,101000060,1 +2787,3,101000070,1 +2788,3,101000080,1 +2789,1,102000010,1 +2790,2,102000020,1 +2791,2,102000030,1 +2792,2,102000040,1 +2793,3,102000050,1 +2794,3,102000060,1 +2795,3,102000070,1 +2796,3,102000080,1 +2797,1,103000010,1 +2798,2,103000020,1 +2799,2,103000030,1 +2800,2,103000040,1 +2801,3,103000050,1 +2802,3,103000060,1 +2803,3,103000070,1 +2804,3,103000080,1 +2805,1,104000010,1 +2806,2,104000020,1 +2807,2,104000030,1 +2808,2,104000040,1 +2809,3,104000050,1 +2810,3,104000060,1 +2811,3,104000070,1 +2812,3,104000080,1 +2813,1,105000010,1 +2814,2,105000020,1 +2815,2,105000030,1 +2816,2,105000040,1 +2817,3,105000050,1 +2818,3,105000060,1 +2819,3,105000070,1 +2820,3,105000080,1 +2821,1,106000010,1 +2822,2,106000020,1 +2823,2,106000030,1 +2824,2,106000040,1 +2825,3,106000050,1 +2826,3,106000060,1 +2827,3,106000070,1 +2828,3,106000080,1 +2829,1,107000010,1 +2830,2,107000020,1 +2831,2,107000030,1 +2832,2,107000040,1 +2833,3,107000050,1 +2834,3,107000060,1 +2835,3,107000070,1 +2836,3,107000080,1 +2837,1,108000010,1 +2838,2,108000020,1 +2839,2,108000030,1 +2840,2,108000040,1 +2841,3,108000050,1 +2842,3,108000060,1 +2843,3,108000070,1 +2844,3,108000080,1 +2845,1,109000010,1 +2846,2,109000020,1 +2847,2,109000030,1 +2848,2,109000040,1 +2849,3,109000050,1 +2850,3,109000060,1 +2851,3,109000070,1 +2852,3,109000080,1 +2853,1,110000010,1 +2854,2,110000020,1 +2855,2,110000030,1 +2856,2,110000040,1 +2857,3,110000050,1 +2858,3,110000060,1 +2859,3,110000070,1 +2860,3,110000080,1 +2861,2,180001,3 +2863,1,107000000,2 +2864,2,107000001,2 +2865,3,107000002,2 +2866,4,107000003,2 +2867,5,107000004,2 +2868,1,120000000,2 +2869,2,120000001,2 +2870,3,120000002,2 +2871,4,120000003,2 +2872,5,120000004,2 +2874,3,110000070,1 +2875,3,110000080,1 +2876,4,110000100,1 +2877,5,110000110,1 +2878,1,107000000,2 +2879,2,107000001,2 +2880,3,107000002,2 +2881,4,107000003,2 +2882,5,107000004,2 +2883,1,120000000,2 +2884,2,120000001,2 +2885,3,120000002,2 +2886,4,120000003,2 +2887,5,120000004,2 +2888,3,120023,3 +2889,3,120033,3 +2890,3,120043,3 +2891,3,120053,3 +2892,4,120024,3 +2893,4,120034,3 +2894,4,120044,3 +2895,4,120054,3 +2896,3,120240,3 +2897,3,120250,3 +2898,3,120260,3 +2899,3,120270,3 +2900,3,120300,3 +2901,3,120310,3 +2902,3,120320,3 +2903,3,120330,3 +2904,3,120340,3 +2905,3,120350,3 +2906,3,120360,3 +2907,3,120370,3 +2908,3,120380,3 +2909,3,120390,3 +2910,3,120400,3 +2911,3,120410,3 +2912,3,120420,3 +2913,3,120430,3 +2914,3,120450,3 +2915,3,120460,3 +2916,3,120550,3 +2917,3,120560,3 +2918,3,120570,3 +2919,3,120990,3 +2920,3,121000,3 +2921,3,121010,3 +2922,3,121020,3 +2923,4,140000,3 +2924,4,150010,3 +2925,4,150020,3 +2926,4,150030,3 +2927,4,150040,3 +2929,3,108000060,1 +2930,3,108000070,1 +2931,3,108000080,1 +2932,3,107000050,1 +2933,4,108000100,1 +2934,4,105000090,1 +2935,5,108000110,1 +2936,1,108000000,2 +2937,2,108000001,2 +2938,3,108000002,2 +2939,4,108000003,2 +2940,5,108000004,2 +2941,1,120000000,2 +2942,2,120000001,2 +2943,3,120000002,2 +2944,4,120000003,2 +2945,5,120000004,2 +2946,3,170004,3 +2948,3,102000060,1 +2949,3,102000070,1 +2950,3,102000080,1 +2951,3,103000050,1 +2952,3,105000050,1 +2953,4,102000100,1 +2954,4,102000110,1 +2955,4,106000090,1 +2956,4,109000090,1 +2957,5,102000120,1 +2958,1,102000000,2 +2959,2,102000001,2 +2960,3,102000002,2 +2961,4,102000003,2 +2962,5,102000004,2 +2963,1,112000000,2 +2964,2,112000001,2 +2965,3,112000002,2 +2966,4,112000003,2 +2967,5,112000004,2 +2968,3,170004,3 +2970,3,101000050,1 +2971,3,101000060,1 +2972,3,101000080,1 +2973,3,103000060,1 +2974,3,103000070,1 +2975,3,103000080,1 +2976,3,101000040,1 +2977,3,109000060,1 +2978,3,109000070,1 +2979,3,109000080,1 +2980,3,110000050,1 +2981,4,101000090,1 +2982,4,102000090,1 +2983,4,103000100,1 +2984,4,101000100,1 +2985,4,101000110,1 +2986,4,109000100,1 +2987,5,101000120,1 +2988,5,103000120,1 +2989,5,109000110,1 +2990,1,101000000,2 +2991,2,101000001,2 +2992,3,101000002,2 +2993,4,101000008,2 +2994,5,101000004,2 +2995,1,112000000,2 +2996,2,112000001,2 +2997,3,112000002,2 +2998,4,112000003,2 +2999,5,112000004,2 +3000,3,120023,3 +3001,3,120033,3 +3002,3,120043,3 +3003,3,120053,3 +3004,4,120024,3 +3005,4,120034,3 +3006,4,120044,3 +3007,4,120054,3 +3008,3,120240,3 +3009,3,120250,3 +3010,3,120260,3 +3011,3,120270,3 +3012,3,120300,3 +3013,3,120310,3 +3014,3,120320,3 +3015,3,120330,3 +3016,3,120340,3 +3017,3,120350,3 +3018,3,120360,3 +3019,3,120370,3 +3020,3,120380,3 +3021,3,120390,3 +3022,3,120400,3 +3023,3,120410,3 +3024,3,120420,3 +3025,3,120430,3 +3026,3,120450,3 +3027,3,120460,3 +3028,3,120550,3 +3029,3,120560,3 +3030,3,120570,3 +3031,3,120990,3 +3032,3,121000,3 +3033,3,121010,3 +3034,3,121020,3 +3035,4,140000,3 +3036,4,150010,3 +3037,4,150020,3 +3038,4,150030,3 +3039,4,150040,3 +3041,3,105000060,1 +3042,3,105000070,1 +3043,3,105000080,1 +3044,3,104000050,1 +3045,3,106000050,1 +3046,4,105000100,1 +3047,4,105000110,1 +3048,4,108000090,1 +3049,4,110000090,1 +3050,5,105000120,1 +3051,1,109000000,2 +3052,2,109000001,2 +3053,3,109000002,2 +3054,4,109000003,2 +3055,5,109000004,2 +3056,1,112000000,2 +3057,2,112000001,2 +3058,3,112000002,2 +3059,4,112000003,2 +3060,5,112000004,2 +3061,3,170004,3 +3063,3,107000060,1 +3064,3,107000070,1 +3065,3,107000080,1 +3066,3,108000050,1 +3067,3,109000050,1 +3068,4,107000100,1 +3069,4,103000090,1 +3070,5,107000110,1 +3071,1,105000000,2 +3072,2,105000001,2 +3073,3,105000002,2 +3074,4,105000003,2 +3075,5,105000004,2 +3076,1,120000000,2 +3077,2,120000001,2 +3078,3,120000002,2 +3079,4,120000003,2 +3080,5,120000004,2 +3081,3,130023,3 +3082,3,130033,3 +3083,3,130043,3 +3084,3,130053,3 +3085,4,130024,3 +3086,4,130034,3 +3087,4,130044,3 +3088,4,130054,3 +3089,3,130060,3 +3090,3,130070,3 +3091,3,130080,3 +3092,3,130090,3 +3093,3,130100,3 +3094,3,130110,3 +3095,3,130120,3 +3096,3,130130,3 +3097,3,130140,3 +3098,3,130150,3 +3099,3,130160,3 +3100,3,130170,3 +3101,3,130180,3 +3102,3,130190,3 +3103,3,130200,3 +3104,3,130420,3 +3105,3,130510,3 +3106,3,130520,3 +3107,3,130530,3 +3108,3,130540,3 +3109,3,130660,3 +3110,4,140000,3 +3111,4,150010,3 +3112,4,150020,3 +3113,4,150030,3 +3114,4,150040,3 +3116,3,106000060,1 +3117,3,106000070,1 +3118,3,106000080,1 +3119,3,101000070,1 +3120,3,110000060,1 +3121,4,106000100,1 +3122,4,106000110,1 +3123,4,104000090,1 +3124,5,106000120,1 +3125,1,103000000,2 +3126,2,103000001,2 +3127,3,103000002,2 +3128,4,103000003,2 +3129,5,103000004,2 +3130,1,112000000,2 +3131,2,112000001,2 +3132,3,112000002,2 +3133,4,112000003,2 +3134,5,112000004,2 +3135,3,170004,3 +3137,3,104000060,1 +3138,3,104000070,1 +3139,3,104000080,1 +3140,3,102000050,1 +3141,4,104000100,1 +3142,4,104000110,1 +3143,4,107000090,1 +3144,5,104000120,1 +3145,1,111000000,2 +3146,2,111000001,2 +3147,3,111000002,2 +3148,4,111000003,2 +3149,5,111000004,2 +3150,1,120000000,2 +3151,2,120000001,2 +3152,3,120000002,2 +3153,4,120000003,2 +3154,5,120000004,2 +3155,3,110023,3 +3156,3,110033,3 +3157,3,110043,3 +3158,3,110053,3 +3159,4,110024,3 +3160,4,110034,3 +3161,4,110044,3 +3162,4,110054,3 +3163,3,110060,3 +3164,3,110070,3 +3165,3,110080,3 +3166,3,110090,3 +3167,3,110100,3 +3168,3,110110,3 +3169,3,110120,3 +3170,3,110130,3 +3171,3,110140,3 +3172,3,110150,3 +3173,3,110160,3 +3174,3,110170,3 +3175,3,110180,3 +3176,3,110190,3 +3177,3,110200,3 +3178,3,110210,3 +3179,3,110220,3 +3180,3,110230,3 +3181,3,110240,3 +3182,3,110250,3 +3183,3,110260,3 +3184,3,110270,3 +3185,3,110620,3 +3186,3,110670,3 +3187,4,140000,3 +3188,4,150010,3 +3189,4,150020,3 +3190,4,150030,3 +3191,4,150040,3 +3193,3,101000050,1 +3194,3,101000060,1 +3195,3,101000080,1 +3196,3,103000060,1 +3197,3,103000070,1 +3198,3,103000080,1 +3199,3,101000040,1 +3200,3,109000060,1 +3201,3,109000070,1 +3202,3,109000080,1 +3203,3,110000050,1 +3204,4,101000090,1 +3205,4,102000090,1 +3206,4,103000100,1 +3207,4,101000100,1 +3208,4,101000110,1 +3209,4,109000100,1 +3210,5,101000120,1 +3211,5,103000120,1 +3212,5,109000110,1 +3213,1,101000000,2 +3214,2,101000001,2 +3215,3,101000002,2 +3216,4,101000008,2 +3217,5,101000004,2 +3218,1,120000000,2 +3219,2,120000001,2 +3220,3,120000002,2 +3221,4,120000003,2 +3222,5,120000004,2 +3223,3,170004,3 +3225,3,110000070,1 +3226,3,110000080,1 +3227,4,110000100,1 +3228,5,110000110,1 +3229,1,107000000,2 +3230,2,107000001,2 +3231,3,107000002,2 +3232,4,107000003,2 +3233,5,107000004,2 +3234,1,120000000,2 +3235,2,120000001,2 +3236,3,120000002,2 +3237,4,120000003,2 +3238,5,120000004,2 +3239,3,180004,3 +3241,3,105000060,1 +3242,3,105000070,1 +3243,3,105000080,1 +3244,3,104000050,1 +3245,3,106000050,1 +3246,4,105000100,1 +3247,4,105000110,1 +3248,4,108000090,1 +3249,4,110000090,1 +3250,5,105000120,1 +3251,1,109000000,2 +3252,2,109000001,2 +3253,3,109000002,2 +3254,4,109000003,2 +3255,5,109000004,2 +3256,1,112000000,2 +3257,2,112000001,2 +3258,3,112000002,2 +3259,4,112000003,2 +3260,5,112000004,2 +3261,3,170004,3 +3263,3,108000060,1 +3264,3,108000070,1 +3265,3,108000080,1 +3266,3,107000050,1 +3267,4,108000100,1 +3268,4,105000090,1 +3269,5,108000110,1 +3270,1,108000000,2 +3271,2,108000001,2 +3272,3,108000002,2 +3273,4,108000003,2 +3274,5,108000004,2 +3275,1,120000000,2 +3276,2,120000001,2 +3277,3,120000002,2 +3278,4,120000003,2 +3279,5,120000004,2 +3280,4,120024,3 +3281,4,120034,3 +3282,4,120044,3 +3283,4,120054,3 +3284,3,120240,3 +3285,3,120250,3 +3286,3,120260,3 +3287,3,120270,3 +3288,3,120300,3 +3289,3,120310,3 +3290,3,120320,3 +3291,3,120330,3 +3292,3,120340,3 +3293,3,120350,3 +3294,3,120360,3 +3295,3,120370,3 +3296,3,120380,3 +3297,3,120390,3 +3298,3,120400,3 +3299,3,120410,3 +3300,3,120420,3 +3301,3,120430,3 +3302,3,120450,3 +3303,3,120460,3 +3304,3,120550,3 +3305,3,120560,3 +3306,3,120570,3 +3307,3,120990,3 +3308,3,121000,3 +3309,3,121010,3 +3310,3,121020,3 +3311,4,140000,3 +3312,4,150010,3 +3313,4,150020,3 +3314,4,150030,3 +3315,4,150040,3 +3317,3,104000060,1 +3318,3,104000070,1 +3319,3,104000080,1 +3320,3,102000050,1 +3321,4,104000100,1 +3322,4,104000110,1 +3323,4,107000090,1 +3324,5,104000120,1 +3325,1,111000000,2 +3326,2,111000001,2 +3327,3,111000002,2 +3328,4,111000003,2 +3329,5,111000004,2 +3330,1,120000000,2 +3331,2,120000001,2 +3332,3,120000002,2 +3333,4,120000003,2 +3334,5,120000004,2 +3335,4,110024,3 +3336,4,110034,3 +3337,4,110044,3 +3338,4,110054,3 +3339,3,110060,3 +3340,3,110070,3 +3341,3,110080,3 +3342,3,110090,3 +3343,3,110100,3 +3344,3,110110,3 +3345,3,110120,3 +3346,3,110130,3 +3347,3,110140,3 +3348,3,110150,3 +3349,3,110160,3 +3350,3,110170,3 +3351,3,110180,3 +3352,3,110190,3 +3353,3,110200,3 +3354,3,110210,3 +3355,3,110220,3 +3356,3,110230,3 +3357,3,110240,3 +3358,3,110250,3 +3359,3,110260,3 +3360,3,110270,3 +3361,3,110620,3 +3362,3,110670,3 +3363,4,140000,3 +3364,4,150010,3 +3365,4,150020,3 +3366,4,150030,3 +3367,4,150040,3 +3369,3,101000050,1 +3370,3,101000060,1 +3371,3,101000080,1 +3372,3,103000060,1 +3373,3,103000070,1 +3374,3,103000080,1 +3375,3,101000040,1 +3376,3,109000060,1 +3377,3,109000070,1 +3378,3,109000080,1 +3379,3,110000050,1 +3380,4,101000090,1 +3381,4,102000090,1 +3382,4,103000100,1 +3383,4,101000100,1 +3384,4,101000110,1 +3385,4,109000100,1 +3386,5,101000120,1 +3387,5,103000120,1 +3388,5,109000110,1 +3389,1,101000000,2 +3390,2,101000001,2 +3391,3,101000002,2 +3392,4,101000008,2 +3393,5,101000004,2 +3394,1,112000000,2 +3395,2,112000001,2 +3396,3,112000002,2 +3397,4,112000003,2 +3398,5,112000004,2 +3399,3,170004,3 +3401,3,107000060,1 +3402,3,107000070,1 +3403,3,107000080,1 +3404,3,108000050,1 +3405,3,109000050,1 +3406,4,107000100,1 +3407,4,103000090,1 +3408,5,107000110,1 +3409,1,105000000,2 +3410,2,105000001,2 +3411,3,105000002,2 +3412,4,105000003,2 +3413,5,105000004,2 +3414,1,120000000,2 +3415,2,120000001,2 +3416,3,120000002,2 +3417,4,120000003,2 +3418,5,120000004,2 +3419,4,120024,3 +3420,4,120034,3 +3421,4,120044,3 +3422,4,120054,3 +3423,3,120240,3 +3424,3,120250,3 +3425,3,120260,3 +3426,3,120270,3 +3427,3,120300,3 +3428,3,120310,3 +3429,3,120320,3 +3430,3,120330,3 +3431,3,120340,3 +3432,3,120350,3 +3433,3,120360,3 +3434,3,120370,3 +3435,3,120380,3 +3436,3,120390,3 +3437,3,120400,3 +3438,3,120410,3 +3439,3,120420,3 +3440,3,120430,3 +3441,3,120450,3 +3442,3,120460,3 +3443,3,120550,3 +3444,3,120560,3 +3445,3,120570,3 +3446,3,120990,3 +3447,3,121000,3 +3448,3,121010,3 +3449,3,121020,3 +3450,4,140000,3 +3451,4,150010,3 +3452,4,150020,3 +3453,4,150030,3 +3454,4,150040,3 +3456,3,108000060,1 +3457,3,108000070,1 +3458,3,108000080,1 +3459,3,107000050,1 +3460,4,108000100,1 +3461,4,105000090,1 +3462,5,108000110,1 +3463,1,108000000,2 +3464,2,108000001,2 +3465,3,108000002,2 +3466,4,108000003,2 +3467,5,108000004,2 +3468,1,120000000,2 +3469,2,120000001,2 +3470,3,120000002,2 +3471,4,120000003,2 +3472,5,120000004,2 +3473,3,170004,3 +3475,3,102000060,1 +3476,3,102000070,1 +3477,3,102000080,1 +3478,3,103000050,1 +3479,3,105000050,1 +3480,4,102000100,1 +3481,4,102000110,1 +3482,4,106000090,1 +3483,4,109000090,1 +3484,5,102000120,1 +3485,1,102000000,2 +3486,2,102000001,2 +3487,3,102000002,2 +3488,4,102000003,2 +3489,5,102000004,2 +3490,1,112000000,2 +3491,2,112000001,2 +3492,3,112000002,2 +3493,4,112000003,2 +3494,5,112000004,2 +3495,3,180004,3 +3497,3,106000060,1 +3498,3,106000070,1 +3499,3,106000080,1 +3500,3,101000070,1 +3501,3,110000060,1 +3502,4,106000100,1 +3503,4,106000110,1 +3504,4,104000090,1 +3505,5,106000120,1 +3506,1,103000000,2 +3507,2,103000001,2 +3508,3,103000002,2 +3509,4,103000003,2 +3510,5,103000004,2 +3511,1,112000000,2 +3512,2,112000001,2 +3513,3,112000002,2 +3514,4,112000003,2 +3515,5,112000004,2 +3516,4,130024,3 +3517,4,130034,3 +3518,4,130044,3 +3519,4,130054,3 +3520,3,130060,3 +3521,3,130070,3 +3522,3,130080,3 +3523,3,130090,3 +3524,3,130100,3 +3525,3,130110,3 +3526,3,130120,3 +3527,3,130130,3 +3528,3,130140,3 +3529,3,130150,3 +3530,3,130160,3 +3531,3,130170,3 +3532,3,130180,3 +3533,3,130190,3 +3534,3,130200,3 +3535,3,130420,3 +3536,3,130510,3 +3537,3,130520,3 +3538,3,130530,3 +3539,3,130540,3 +3540,3,130660,3 +3541,4,140000,3 +3542,4,150010,3 +3543,4,150020,3 +3544,4,150030,3 +3545,4,150040,3 +3547,3,110000070,1 +3548,3,110000080,1 +3549,4,110000100,1 +3550,5,110000110,1 +3551,1,107000000,2 +3552,2,107000001,2 +3553,3,107000002,2 +3554,4,107000003,2 +3555,5,107000004,2 +3556,1,120000000,2 +3557,2,120000001,2 +3558,3,120000002,2 +3559,4,120000003,2 +3560,5,120000004,2 +3561,3,170004,3 +3563,3,105000060,1 +3564,3,105000070,1 +3565,3,105000080,1 +3566,3,104000050,1 +3567,3,106000050,1 +3568,4,105000100,1 +3569,4,105000110,1 +3570,4,108000090,1 +3571,4,110000090,1 +3572,5,105000120,1 +3573,1,109000000,2 +3574,2,109000001,2 +3575,3,109000002,2 +3576,4,109000003,2 +3577,5,109000004,2 +3578,1,112000000,2 +3579,2,112000001,2 +3580,3,112000002,2 +3581,4,112000003,2 +3582,5,112000004,2 +3583,4,120024,3 +3584,4,120034,3 +3585,4,120044,3 +3586,4,120054,3 +3587,3,120240,3 +3588,3,120250,3 +3589,3,120260,3 +3590,3,120270,3 +3591,3,120300,3 +3592,3,120310,3 +3593,3,120320,3 +3594,3,120330,3 +3595,3,120340,3 +3596,3,120350,3 +3597,3,120360,3 +3598,3,120370,3 +3599,3,120380,3 +3600,3,120390,3 +3601,3,120400,3 +3602,3,120410,3 +3603,3,120420,3 +3604,3,120430,3 +3605,3,120450,3 +3606,3,120460,3 +3607,3,120550,3 +3608,3,120560,3 +3609,3,120570,3 +3610,3,120990,3 +3611,3,121000,3 +3612,3,121010,3 +3613,3,121020,3 +3614,4,140000,3 +3615,4,150010,3 +3616,4,150020,3 +3617,4,150030,3 +3618,4,150040,3 +3620,1,170000,3 +3621,2,170001,3 +3622,3,170002,3 +3623,4,170003,3 +3624,5,170004,3 +3625,1,180000,3 +3626,2,180001,3 +3627,3,180002,3 +3628,4,180003,3 +3629,5,180004,3 +3630,4,140000,3 +3631,1,201000010,8 +3632,1,292000010,8 +3633,1,299000040,8 +3634,1,299000070,8 +3635,1,299000110,8 +3636,1,299000120,8 +3637,1,299000140,8 +3638,2,202000010,8 +3639,2,290000010,8 +3640,2,299000010,8 +3641,2,299000150,8 +3642,2,299000190,8 +3643,2,299000200,8 +3644,2,299000210,8 +3645,3,298000050,8 +3646,3,298000060,8 +3647,3,299000060,8 +3648,3,299000170,8 +3649,5,150010,3 +3650,5,150020,3 +3651,5,150030,3 +3652,5,150040,3 +3654,3,105000060,1 +3655,3,105000070,1 +3656,3,105000080,1 +3657,3,104000050,1 +3658,3,106000050,1 +3659,4,105000100,1 +3660,4,105000110,1 +3661,4,108000090,1 +3662,4,110000090,1 +3663,5,105000120,1 +3664,1,109000000,2 +3665,2,109000001,2 +3666,3,109000002,2 +3667,4,109000003,2 +3668,5,109000004,2 +3669,1,112000000,2 +3670,2,112000001,2 +3671,3,112000002,2 +3672,4,112000003,2 +3673,5,112000004,2 +3674,3,170004,3 +3676,3,108000060,1 +3677,3,108000070,1 +3678,3,108000080,1 +3679,3,107000050,1 +3680,4,108000100,1 +3681,4,105000090,1 +3682,5,108000110,1 +3683,1,108000000,2 +3684,2,108000001,2 +3685,3,108000002,2 +3686,4,108000003,2 +3687,5,108000004,2 +3688,1,120000000,2 +3689,2,120000001,2 +3690,3,120000002,2 +3691,4,120000003,2 +3692,5,120000004,2 +3693,3,180004,3 +3695,3,106000060,1 +3696,3,106000070,1 +3697,3,106000080,1 +3698,3,101000070,1 +3699,3,110000060,1 +3700,4,106000100,1 +3701,4,106000110,1 +3702,4,104000090,1 +3703,5,106000120,1 +3704,1,103000000,2 +3705,2,103000001,2 +3706,3,103000002,2 +3707,4,103000003,2 +3708,5,103000004,2 +3709,1,112000000,2 +3710,2,112000001,2 +3711,3,112000002,2 +3712,4,112000003,2 +3713,5,112000004,2 +3714,3,170004,3 +3716,3,104000170,1 +3717,1,115000000,2 +3718,2,115000001,2 +3719,3,115000002,2 +3720,4,115000003,2 +3721,5,115000004,2 +3722,1,120000000,2 +3723,2,120000001,2 +3724,3,120000002,2 +3725,4,120000003,2 +3726,5,120000004,2 +3727,4,120024,3 +3728,4,120034,3 +3729,4,120044,3 +3730,4,120054,3 +3731,3,120241,3 +3732,3,120251,3 +3733,3,120261,3 +3734,3,120271,3 +3735,3,120300,3 +3736,3,120310,3 +3737,3,120320,3 +3738,3,120330,3 +3739,3,120340,3 +3740,3,120350,3 +3741,3,120360,3 +3742,3,120370,3 +3743,3,120380,3 +3744,3,120390,3 +3745,3,120400,3 +3746,3,120410,3 +3747,3,120420,3 +3748,3,120430,3 +3749,3,120450,3 +3750,3,120460,3 +3751,3,120550,3 +3752,3,120560,3 +3753,3,120570,3 +3754,3,120990,3 +3755,3,121000,3 +3756,3,121010,3 +3757,3,121020,3 +3758,4,140000,3 +3759,4,150010,3 +3760,4,150020,3 +3761,4,150030,3 +3762,4,150040,3 +3764,3,102000060,1 +3765,3,102000070,1 +3766,3,102000080,1 +3767,3,103000050,1 +3768,3,105000050,1 +3769,4,102000100,1 +3770,4,102000110,1 +3771,4,106000090,1 +3772,4,109000090,1 +3773,5,102000120,1 +3774,1,102000000,2 +3775,2,102000001,2 +3776,3,102000002,2 +3777,4,102000003,2 +3778,5,102000004,2 +3779,1,112000000,2 +3780,2,112000001,2 +3781,3,112000002,2 +3782,4,112000003,2 +3783,5,112000004,2 +3784,4,110024,3 +3785,4,110034,3 +3786,4,110044,3 +3787,4,110054,3 +3788,3,110060,3 +3789,3,110070,3 +3790,3,110080,3 +3791,3,110090,3 +3792,3,110100,3 +3793,3,110110,3 +3794,3,110120,3 +3795,3,110130,3 +3796,3,110140,3 +3797,3,110150,3 +3798,3,110160,3 +3799,3,110170,3 +3800,3,110180,3 +3801,3,110190,3 +3802,3,110200,3 +3803,3,110210,3 +3804,3,110220,3 +3805,3,110230,3 +3806,3,110240,3 +3807,3,110250,3 +3808,3,110260,3 +3809,3,110270,3 +3810,3,110620,3 +3811,3,110670,3 +3812,4,140000,3 +3813,4,150010,3 +3814,4,150020,3 +3815,4,150030,3 +3816,4,150040,3 +3818,3,104000060,1 +3819,3,104000070,1 +3820,3,104000080,1 +3821,3,102000050,1 +3822,4,104000100,1 +3823,4,104000110,1 +3824,4,107000090,1 +3825,5,104000120,1 +3826,1,111000000,2 +3827,2,111000001,2 +3828,3,111000002,2 +3829,4,111000003,2 +3830,5,111000004,2 +3831,1,120000000,2 +3832,2,120000001,2 +3833,3,120000002,2 +3834,4,120000003,2 +3835,5,120000004,2 +3836,4,110024,3 +3837,4,110034,3 +3838,4,110044,3 +3839,4,110054,3 +3840,3,110060,3 +3841,3,110070,3 +3842,3,110080,3 +3843,3,110090,3 +3844,3,110100,3 +3845,3,110110,3 +3846,3,110120,3 +3847,3,110130,3 +3848,3,110140,3 +3849,3,110150,3 +3850,3,110160,3 +3851,3,110170,3 +3852,3,110180,3 +3853,3,110190,3 +3854,3,110200,3 +3855,3,110210,3 +3856,3,110220,3 +3857,3,110230,3 +3858,3,110240,3 +3859,3,110250,3 +3860,3,110260,3 +3861,3,110270,3 +3862,3,110620,3 +3863,3,110670,3 +3864,4,140000,3 +3865,4,150010,3 +3866,4,150020,3 +3867,4,150030,3 +3868,4,150040,3 +3870,3,110000070,1 +3871,3,110000080,1 +3872,4,110000100,1 +3873,5,110000110,1 +3874,1,107000000,2 +3875,2,107000001,2 +3876,3,107000002,2 +3877,4,107000003,2 +3878,5,107000004,2 +3879,1,120000000,2 +3880,2,120000001,2 +3881,3,120000002,2 +3882,4,120000003,2 +3883,5,120000004,2 +3884,4,130024,3 +3885,4,130034,3 +3886,4,130044,3 +3887,4,130054,3 +3888,3,130060,3 +3889,3,130070,3 +3890,3,130080,3 +3891,3,130090,3 +3892,3,130100,3 +3893,3,130110,3 +3894,3,130120,3 +3895,3,130130,3 +3896,3,130140,3 +3897,3,130150,3 +3898,3,130160,3 +3899,3,130170,3 +3900,3,130180,3 +3901,3,130190,3 +3902,3,130200,3 +3903,3,130420,3 +3904,3,130510,3 +3905,3,130520,3 +3906,3,130530,3 +3907,3,130540,3 +3908,3,130660,3 +3909,4,140000,3 +3910,4,150010,3 +3911,4,150020,3 +3912,4,150030,3 +3913,4,150040,3 +3915,3,101000050,1 +3916,3,101000060,1 +3917,3,101000080,1 +3918,3,103000060,1 +3919,3,103000070,1 +3920,3,103000080,1 +3921,3,101000040,1 +3922,3,109000060,1 +3923,3,109000070,1 +3924,3,109000080,1 +3925,3,110000050,1 +3926,4,101000090,1 +3927,4,102000090,1 +3928,4,103000100,1 +3929,4,101000100,1 +3930,4,101000110,1 +3931,4,109000100,1 +3932,5,101000120,1 +3933,5,101000160,1 +3934,5,103000120,1 +3935,5,109000110,1 +3936,1,101000000,2 +3937,2,101000001,2 +3938,3,101000002,2 +3939,4,101000008,2 +3940,5,101000004,2 +3941,1,120000000,2 +3942,2,120000001,2 +3943,3,120000002,2 +3944,4,120000003,2 +3945,5,120000004,2 +3946,3,170004,3 +3948,3,107000060,1 +3949,3,107000070,1 +3950,3,107000080,1 +3951,3,108000050,1 +3952,3,109000050,1 +3953,4,107000100,1 +3954,4,103000090,1 +3955,5,107000110,1 +3956,1,105000000,2 +3957,2,105000001,2 +3958,3,105000002,2 +3959,4,105000003,2 +3960,5,105000004,2 +3961,1,120000000,2 +3962,2,120000001,2 +3963,3,120000002,2 +3964,4,120000003,2 +3965,5,120000004,2 +3966,3,170004,3 +3968,3,104000170,1 +3969,1,115000000,2 +3970,2,115000001,2 +3971,3,115000002,2 +3972,4,115000003,2 +3973,5,115000004,2 +3974,1,120000000,2 +3975,2,120000001,2 +3976,3,120000002,2 +3977,4,120000003,2 +3978,5,120000004,2 +3979,4,120024,3 +3980,4,120034,3 +3981,4,120044,3 +3982,4,120054,3 +3983,3,120241,3 +3984,3,120251,3 +3985,3,120261,3 +3986,3,120271,3 +3987,3,120300,3 +3988,3,120310,3 +3989,3,120320,3 +3990,3,120330,3 +3991,3,120340,3 +3992,3,120350,3 +3993,3,120360,3 +3994,3,120370,3 +3995,3,120380,3 +3996,3,120390,3 +3997,3,120400,3 +3998,3,120410,3 +3999,3,120420,3 +4000,3,120430,3 +4001,3,120450,3 +4002,3,120460,3 +4003,3,120550,3 +4004,3,120560,3 +4005,3,120570,3 +4006,3,120990,3 +4007,3,121000,3 +4008,3,121010,3 +4009,3,121020,3 +4010,4,140000,3 +4011,4,150010,3 +4012,4,150020,3 +4013,4,150030,3 +4014,4,150040,3 +4016,1,101000000,2 +4017,2,101000001,2 +4018,3,101000002,2 +4019,4,101000008,2 +4020,5,101000012,2 +4021,1,120000000,2 +4022,2,120000001,2 +4023,3,120000002,2 +4024,4,120000003,2 +4025,5,120000004,2 +4027,1,101000000,2 +4028,2,101000001,2 +4029,3,101000002,2 +4030,4,101000008,2 +4031,5,101000011,2 +4032,1,120000000,2 +4033,2,120000001,2 +4034,3,120000002,2 +4035,4,120000003,2 +4036,5,120000004,2 +4038,3,101000050,1 +4039,3,101000060,1 +4040,3,101000080,1 +4041,3,103000060,1 +4042,3,103000070,1 +4043,3,103000080,1 +4044,3,101000040,1 +4045,3,109000060,1 +4046,3,109000070,1 +4047,3,109000080,1 +4048,3,110000050,1 +4049,4,101000090,1 +4050,4,102000090,1 +4051,4,103000100,1 +4052,4,101000100,1 +4053,4,101000110,1 +4054,4,109000100,1 +4055,5,101000120,1 +4056,5,101000160,1 +4057,5,103000120,1 +4058,5,109000110,1 +4059,1,101000000,2 +4060,2,101000001,2 +4061,3,101000002,2 +4062,4,101000008,2 +4063,5,101000004,2 +4064,1,120000000,2 +4065,2,120000001,2 +4066,3,120000002,2 +4067,4,120000003,2 +4068,5,120000004,2 +4069,4,120024,3 +4070,4,120034,3 +4071,4,120044,3 +4072,4,120054,3 +4073,3,120241,3 +4074,3,120251,3 +4075,3,120261,3 +4076,3,120271,3 +4077,3,120300,3 +4078,3,120310,3 +4079,3,120320,3 +4080,3,120330,3 +4081,3,120340,3 +4082,3,120350,3 +4083,3,120360,3 +4084,3,120370,3 +4085,3,120380,3 +4086,3,120390,3 +4087,3,120400,3 +4088,3,120410,3 +4089,3,120420,3 +4090,3,120430,3 +4091,3,120450,3 +4092,3,120460,3 +4093,3,120550,3 +4094,3,120560,3 +4095,3,120570,3 +4096,3,120990,3 +4097,3,121000,3 +4098,3,121010,3 +4099,3,121020,3 +4100,4,140000,3 +4101,4,150010,3 +4102,4,150020,3 +4103,4,150030,3 +4104,4,150040,3 +4106,3,108000060,1 +4107,3,108000070,1 +4108,3,108000080,1 +4109,3,107000050,1 +4110,4,108000100,1 +4111,4,105000090,1 +4112,5,108000110,1 +4113,1,108000000,2 +4114,2,108000001,2 +4115,3,108000002,2 +4116,4,108000003,2 +4117,5,108000004,2 +4118,1,120000000,2 +4119,2,120000001,2 +4120,3,120000002,2 +4121,4,120000003,2 +4122,5,120000004,2 +4123,3,170004,3 +4125,3,102000060,1 +4126,3,102000070,1 +4127,3,102000080,1 +4128,3,103000050,1 +4129,3,105000050,1 +4130,4,102000100,1 +4131,4,102000110,1 +4132,4,106000090,1 +4133,4,109000090,1 +4134,5,102000120,1 +4135,1,102000000,2 +4136,2,102000001,2 +4137,3,102000002,2 +4138,4,102000003,2 +4139,5,102000004,2 +4140,1,112000000,2 +4141,2,112000001,2 +4142,3,112000002,2 +4143,4,112000003,2 +4144,5,112000004,2 +4145,4,110024,3 +4146,4,110034,3 +4147,4,110044,3 +4148,4,110054,3 +4149,3,110060,3 +4150,3,110070,3 +4151,3,110080,3 +4152,3,110090,3 +4153,3,110100,3 +4154,3,110110,3 +4155,3,110120,3 +4156,3,110130,3 +4157,3,110140,3 +4158,3,110150,3 +4159,3,110160,3 +4160,3,110170,3 +4161,3,110180,3 +4162,3,110190,3 +4163,3,110200,3 +4164,3,110210,3 +4165,3,110220,3 +4166,3,110230,3 +4167,3,110240,3 +4168,3,110250,3 +4169,3,110260,3 +4170,3,110270,3 +4171,3,110620,3 +4172,3,110670,3 +4173,4,140000,3 +4174,4,150010,3 +4175,4,150020,3 +4176,4,150030,3 +4177,4,150040,3 +4179,3,104000170,1 +4180,4,104000180,1 +4181,5,104000190,1 +4182,1,115000000,2 +4183,2,115000001,2 +4184,3,115000002,2 +4185,4,115000003,2 +4186,5,115000004,2 +4187,1,120000000,2 +4188,2,120000001,2 +4189,3,120000002,2 +4190,4,120000003,2 +4191,5,120000004,2 +4192,4,120024,3 +4193,4,120034,3 +4194,4,120044,3 +4195,4,120054,3 +4196,3,120241,3 +4197,3,120251,3 +4198,3,120261,3 +4199,3,120271,3 +4200,3,120300,3 +4201,3,120310,3 +4202,3,120320,3 +4203,3,120330,3 +4204,3,120340,3 +4205,3,120350,3 +4206,3,120360,3 +4207,3,120370,3 +4208,3,120380,3 +4209,3,120390,3 +4210,3,120400,3 +4211,3,120410,3 +4212,3,120420,3 +4213,3,120430,3 +4214,3,120450,3 +4215,3,120460,3 +4216,3,120550,3 +4217,3,120560,3 +4218,3,120570,3 +4219,3,120990,3 +4220,3,121000,3 +4221,3,121010,3 +4222,3,121020,3 +4223,4,140000,3 +4224,4,150010,3 +4225,4,150020,3 +4226,4,150030,3 +4227,4,150040,3 +4229,3,110000070,1 +4230,3,110000080,1 +4231,4,110000100,1 +4232,5,110000110,1 +4233,1,107000000,2 +4234,2,107000001,2 +4235,3,107000002,2 +4236,4,107000003,2 +4237,5,107000004,2 +4238,1,120000000,2 +4239,2,120000001,2 +4240,3,120000002,2 +4241,4,120000003,2 +4242,5,120000004,2 +4243,4,120024,3 +4244,4,120034,3 +4245,4,120044,3 +4246,4,120054,3 +4247,3,120241,3 +4248,3,120251,3 +4249,3,120261,3 +4250,3,120271,3 +4251,3,120300,3 +4252,3,120310,3 +4253,3,120320,3 +4254,3,120330,3 +4255,3,120340,3 +4256,3,120350,3 +4257,3,120360,3 +4258,3,120370,3 +4259,3,120380,3 +4260,3,120390,3 +4261,3,120400,3 +4262,3,120410,3 +4263,3,120420,3 +4264,3,120430,3 +4265,3,120450,3 +4266,3,120460,3 +4267,3,120550,3 +4268,3,120560,3 +4269,3,120570,3 +4270,3,120990,3 +4271,3,121000,3 +4272,3,121010,3 +4273,3,121020,3 +4274,4,140000,3 +4275,4,150010,3 +4276,4,150020,3 +4277,4,150030,3 +4278,4,150040,3 +4280,3,106000060,1 +4281,3,106000070,1 +4282,3,106000080,1 +4283,3,101000070,1 +4284,3,110000060,1 +4285,4,106000100,1 +4286,4,106000110,1 +4287,4,104000090,1 +4288,5,106000120,1 +4289,1,103000000,2 +4290,2,103000001,2 +4291,3,103000002,2 +4292,4,103000003,2 +4293,5,103000004,2 +4294,1,112000000,2 +4295,2,112000001,2 +4296,3,112000002,2 +4297,4,112000003,2 +4298,5,112000004,2 +4299,3,170004,3 +4301,3,105000060,1 +4302,3,105000070,1 +4303,3,105000080,1 +4304,3,104000050,1 +4305,3,106000050,1 +4306,4,105000100,1 +4307,4,105000110,1 +4308,4,108000090,1 +4309,4,110000090,1 +4310,5,105000120,1 +4311,1,109000000,2 +4312,2,109000001,2 +4313,3,109000002,2 +4314,4,109000003,2 +4315,5,109000004,2 +4316,1,112000000,2 +4317,2,112000001,2 +4318,3,112000002,2 +4319,4,112000003,2 +4320,5,112000004,2 +4321,4,130024,3 +4322,4,130034,3 +4323,4,130044,3 +4324,4,130054,3 +4325,3,130060,3 +4326,3,130070,3 +4327,3,130080,3 +4328,3,130090,3 +4329,3,130100,3 +4330,3,130110,3 +4331,3,130120,3 +4332,3,130130,3 +4333,3,130140,3 +4334,3,130150,3 +4335,3,130160,3 +4336,3,130170,3 +4337,3,130180,3 +4338,3,130190,3 +4339,3,130200,3 +4340,3,130420,3 +4341,3,130510,3 +4342,3,130520,3 +4343,3,130530,3 +4344,3,130540,3 +4345,3,130660,3 +4346,4,140000,3 +4347,4,150010,3 +4348,4,150020,3 +4349,4,150030,3 +4350,4,150040,3 +4352,3,101000050,1 +4353,3,101000060,1 +4354,3,101000080,1 +4355,3,103000060,1 +4356,3,103000070,1 +4357,3,103000080,1 +4358,3,101000040,1 +4359,3,109000060,1 +4360,3,109000070,1 +4361,3,109000080,1 +4362,3,110000050,1 +4363,4,101000090,1 +4364,4,102000090,1 +4365,4,103000100,1 +4366,4,101000100,1 +4367,4,101000110,1 +4368,4,109000100,1 +4369,5,101000120,1 +4370,5,103000120,1 +4371,5,109000110,1 +4372,1,101000000,2 +4373,2,101000001,2 +4374,3,101000002,2 +4375,4,101000008,2 +4376,5,101000004,2 +4377,1,112000000,2 +4378,2,112000001,2 +4379,3,112000002,2 +4380,4,112000003,2 +4381,5,112000004,2 +4382,3,180004,3 +4384,3,104000060,1 +4385,3,104000070,1 +4386,3,104000080,1 +4387,3,102000050,1 +4388,4,104000100,1 +4389,4,104000110,1 +4390,4,107000090,1 +4391,5,104000120,1 +4392,1,111000000,2 +4393,2,111000001,2 +4394,3,111000002,2 +4395,4,111000003,2 +4396,5,111000004,2 +4397,1,120000000,2 +4398,2,120000001,2 +4399,3,120000002,2 +4400,4,120000003,2 +4401,5,120000004,2 +4402,4,110024,3 +4403,4,110034,3 +4404,4,110044,3 +4405,4,110054,3 +4406,3,110060,3 +4407,3,110070,3 +4408,3,110080,3 +4409,3,110090,3 +4410,3,110100,3 +4411,3,110110,3 +4412,3,110120,3 +4413,3,110130,3 +4414,3,110140,3 +4415,3,110150,3 +4416,3,110160,3 +4417,3,110170,3 +4418,3,110180,3 +4419,3,110190,3 +4420,3,110200,3 +4421,3,110210,3 +4422,3,110220,3 +4423,3,110230,3 +4424,3,110240,3 +4425,3,110250,3 +4426,3,110260,3 +4427,3,110270,3 +4428,3,110620,3 +4429,3,110670,3 +4430,4,140000,3 +4431,4,150010,3 +4432,4,150020,3 +4433,4,150030,3 +4434,4,150040,3 +4436,3,107000060,1 +4437,3,107000070,1 +4438,3,107000080,1 +4439,3,108000050,1 +4440,3,109000050,1 +4441,4,107000100,1 +4442,4,103000090,1 +4443,5,107000110,1 +4444,1,105000000,2 +4445,2,105000001,2 +4446,3,105000002,2 +4447,4,105000003,2 +4448,5,105000004,2 +4449,1,120000000,2 +4450,2,120000001,2 +4451,3,120000002,2 +4452,4,120000003,2 +4453,5,120000004,2 +4454,4,130024,3 +4455,4,130034,3 +4456,4,130044,3 +4457,4,130054,3 +4458,3,130060,3 +4459,3,130070,3 +4460,3,130080,3 +4461,3,130090,3 +4462,3,130100,3 +4463,3,130110,3 +4464,3,130120,3 +4465,3,130130,3 +4466,3,130140,3 +4467,3,130150,3 +4468,3,130160,3 +4469,3,130170,3 +4470,3,130180,3 +4471,3,130190,3 +4472,3,130200,3 +4473,3,130420,3 +4474,3,130510,3 +4475,3,130520,3 +4476,3,130530,3 +4477,3,130540,3 +4478,3,130660,3 +4479,4,140000,3 +4480,4,150010,3 +4481,4,150020,3 +4482,4,150030,3 +4483,4,150040,3 +4485,1,109000010,1 +4486,2,109000020,1 +4487,2,109000030,1 +4488,2,109000040,1 +4489,3,109000050,1 +4490,3,109000060,1 +4491,3,109000070,1 +4492,3,109000080,1 +4493,4,109000090,1 +4494,4,109000100,1 +4495,5,109000110,1 +4496,1,170000,3 +4497,2,170001,3 +4498,3,170002,3 +4499,4,170003,3 +4500,5,170004,3 +4501,1,180000,3 +4502,2,180001,3 +4503,3,180002,3 +4504,4,180003,3 +4505,5,180004,3 +4506,1,201000010,8 +4507,1,292000010,8 +4508,1,299000040,8 +4509,1,299000070,8 +4510,1,299000110,8 +4511,1,299000120,8 +4512,1,299000140,8 +4513,2,202000010,8 +4514,2,290000010,8 +4515,2,299000010,8 +4516,2,299000150,8 +4517,2,299000190,8 +4518,2,299000200,8 +4519,2,299000210,8 +4520,3,298000050,8 +4521,3,298000060,8 +4522,3,299000060,8 +4523,3,299000170,8 +4524,4,140000,3 +4525,5,150010,3 +4526,5,150020,3 +4527,5,150030,3 +4528,5,150040,3 +4530,1,109000010,1 +4531,2,109000020,1 +4532,2,109000030,1 +4533,2,109000040,1 +4534,3,109000050,1 +4535,3,109000060,1 +4536,3,109000070,1 +4537,3,109000080,1 +4538,4,109000090,1 +4539,4,109000100,1 +4540,5,109000110,1 +4541,1,170000,3 +4542,2,170001,3 +4543,3,170002,3 +4544,4,170003,3 +4545,5,170004,3 +4546,1,180000,3 +4547,2,180001,3 +4548,3,180002,3 +4549,4,180003,3 +4550,5,180004,3 +4551,1,201000010,8 +4552,1,292000010,8 +4553,1,299000040,8 +4554,1,299000070,8 +4555,1,299000110,8 +4556,1,299000120,8 +4557,1,299000140,8 +4558,2,202000010,8 +4559,2,290000010,8 +4560,2,299000010,8 +4561,2,299000150,8 +4562,2,299000190,8 +4563,2,299000200,8 +4564,2,299000210,8 +4565,3,298000050,8 +4566,3,298000060,8 +4567,3,299000060,8 +4568,3,299000170,8 +4569,4,140000,3 +4570,5,150010,3 +4571,5,150020,3 +4572,5,150030,3 +4573,5,150040,3 +4575,3,109000050,1 +4576,3,109000060,1 +4577,3,109000070,1 +4578,3,109000080,1 +4579,4,109000090,1 +4580,4,109000100,1 +4581,5,109000110,1 +4582,1,170000,3 +4583,2,170001,3 +4584,3,170002,3 +4585,4,170003,3 +4586,5,170004,3 +4587,1,180000,3 +4588,2,180001,3 +4589,3,180002,3 +4590,4,180003,3 +4591,5,180004,3 +4592,1,201000010,8 +4593,1,292000010,8 +4594,1,299000040,8 +4595,1,299000070,8 +4596,1,299000110,8 +4597,1,299000120,8 +4598,1,299000140,8 +4599,2,202000010,8 +4600,2,290000010,8 +4601,2,299000010,8 +4602,2,299000150,8 +4603,2,299000190,8 +4604,2,299000200,8 +4605,2,299000210,8 +4606,3,298000050,8 +4607,3,298000060,8 +4608,3,299000060,8 +4609,3,299000170,8 +4610,4,140000,3 +4611,5,150010,3 +4612,5,150020,3 +4613,5,150030,3 +4614,5,150040,3 +4616,3,109000050,1 +4617,3,109000060,1 +4618,3,109000070,1 +4619,3,109000080,1 +4620,4,109000090,1 +4621,4,109000100,1 +4622,5,109000110,1 +4623,1,170000,3 +4624,2,170001,3 +4625,3,170002,3 +4626,4,170003,3 +4627,5,170004,3 +4628,1,180000,3 +4629,2,180001,3 +4630,3,180002,3 +4631,4,180003,3 +4632,5,180004,3 +4633,1,201000010,8 +4634,1,292000010,8 +4635,1,299000040,8 +4636,1,299000070,8 +4637,1,299000110,8 +4638,1,299000120,8 +4639,1,299000140,8 +4640,2,202000010,8 +4641,2,290000010,8 +4642,2,299000010,8 +4643,2,299000150,8 +4644,2,299000190,8 +4645,2,299000200,8 +4646,2,299000210,8 +4647,3,298000050,8 +4648,3,298000060,8 +4649,3,299000060,8 +4650,3,299000170,8 +4651,4,140000,3 +4652,5,150010,3 +4653,5,150020,3 +4654,5,150030,3 +4655,5,150040,3 +4657,3,109000050,1 +4658,3,109000060,1 +4659,3,109000070,1 +4660,3,109000080,1 +4661,4,109000090,1 +4662,4,109000100,1 +4663,5,109000110,1 +4664,1,170000,3 +4665,2,170001,3 +4666,3,170002,3 +4667,4,170003,3 +4668,5,170004,3 +4669,1,180000,3 +4670,2,180001,3 +4671,3,180002,3 +4672,4,180003,3 +4673,5,180004,3 +4674,1,201000010,8 +4675,1,292000010,8 +4676,1,299000040,8 +4677,1,299000070,8 +4678,1,299000110,8 +4679,1,299000120,8 +4680,1,299000140,8 +4681,2,202000010,8 +4682,2,290000010,8 +4683,2,299000010,8 +4684,2,299000150,8 +4685,2,299000190,8 +4686,2,299000200,8 +4687,2,299000210,8 +4688,3,298000050,8 +4689,3,298000060,8 +4690,3,299000060,8 +4691,3,299000170,8 +4692,4,140000,3 +4693,5,150010,3 +4694,5,150020,3 +4695,5,150030,3 +4696,5,150040,3 +4697,5,190000,3 +4698,5,200000,3 +4699,5,210000,3 +4701,3,102000060,1 +4702,3,102000070,1 +4703,3,102000080,1 +4704,3,103000050,1 +4705,3,105000050,1 +4706,4,102000100,1 +4707,4,102000110,1 +4708,4,106000090,1 +4709,4,109000090,1 +4710,5,102000120,1 +4711,1,102000000,2 +4712,2,102000001,2 +4713,3,102000002,2 +4714,4,102000003,2 +4715,5,102000004,2 +4716,1,112000000,2 +4717,2,112000001,2 +4718,3,112000002,2 +4719,4,112000003,2 +4720,5,112000004,2 +4721,3,170004,3 +4722,4,170005,3 +4724,3,110000070,1 +4725,3,110000080,1 +4726,4,110000100,1 +4727,5,110000110,1 +4728,1,107000000,2 +4729,2,107000001,2 +4730,3,107000002,2 +4731,4,107000003,2 +4732,5,107000004,2 +4733,1,120000000,2 +4734,2,120000001,2 +4735,3,120000002,2 +4736,4,120000003,2 +4737,5,120000004,2 +4738,4,120024,3 +4739,4,120034,3 +4740,4,120044,3 +4741,4,120054,3 +4742,3,120241,3 +4743,3,120251,3 +4744,3,120261,3 +4745,3,120271,3 +4746,3,120300,3 +4747,3,120310,3 +4748,3,120320,3 +4749,3,120330,3 +4750,3,120340,3 +4751,3,120350,3 +4752,3,120360,3 +4753,3,120370,3 +4754,3,120380,3 +4755,3,120390,3 +4756,3,120400,3 +4757,3,120410,3 +4758,3,120420,3 +4759,3,120430,3 +4760,3,120450,3 +4761,3,120460,3 +4762,3,120550,3 +4763,3,120560,3 +4764,3,120570,3 +4765,3,120990,3 +4766,3,121000,3 +4767,3,121010,3 +4768,3,121020,3 +4769,4,140000,3 +4770,4,150010,3 +4771,4,150020,3 +4772,4,150030,3 +4773,4,150040,3 +4775,3,105000060,1 +4776,3,105000070,1 +4777,3,105000080,1 +4778,3,104000050,1 +4779,3,106000050,1 +4780,4,105000100,1 +4781,4,105000110,1 +4782,4,108000090,1 +4783,4,110000090,1 +4784,5,105000120,1 +4785,1,109000000,2 +4786,2,109000001,2 +4787,3,109000002,2 +4788,4,109000003,2 +4789,5,109000004,2 +4790,1,112000000,2 +4791,2,112000001,2 +4792,3,112000002,2 +4793,4,112000003,2 +4794,5,112000004,2 +4795,4,130024,3 +4796,4,130034,3 +4797,4,130044,3 +4798,4,130054,3 +4799,3,130060,3 +4800,3,130070,3 +4801,3,130080,3 +4802,3,130090,3 +4803,3,130100,3 +4804,3,130110,3 +4805,3,130120,3 +4806,3,130130,3 +4807,3,130140,3 +4808,3,130150,3 +4809,3,130160,3 +4810,3,130170,3 +4811,3,130180,3 +4812,3,130190,3 +4813,3,130200,3 +4814,3,130420,3 +4815,3,130510,3 +4816,3,130520,3 +4817,3,130530,3 +4818,3,130540,3 +4819,3,130660,3 +4820,4,140000,3 +4821,4,150010,3 +4822,4,150020,3 +4823,4,150030,3 +4824,4,150040,3 +4826,3,104000060,1 +4827,3,104000070,1 +4828,3,104000080,1 +4829,3,102000050,1 +4830,4,104000100,1 +4831,4,104000110,1 +4832,4,107000090,1 +4833,5,104000120,1 +4834,1,111000000,2 +4835,2,111000001,2 +4836,3,111000002,2 +4837,4,111000003,2 +4838,5,111000004,2 +4839,1,120000000,2 +4840,2,120000001,2 +4841,3,120000002,2 +4842,4,120000003,2 +4843,5,120000004,2 +4844,4,110024,3 +4845,4,110034,3 +4846,4,110044,3 +4847,4,110054,3 +4848,3,110060,3 +4849,3,110070,3 +4850,3,110080,3 +4851,3,110090,3 +4852,3,110100,3 +4853,3,110110,3 +4854,3,110120,3 +4855,3,110130,3 +4856,3,110140,3 +4857,3,110150,3 +4858,3,110160,3 +4859,3,110170,3 +4860,3,110180,3 +4861,3,110190,3 +4862,3,110200,3 +4863,3,110210,3 +4864,3,110220,3 +4865,3,110230,3 +4866,3,110240,3 +4867,3,110250,3 +4868,3,110260,3 +4869,3,110270,3 +4870,3,110620,3 +4871,3,110670,3 +4872,4,140000,3 +4873,4,150010,3 +4874,4,150020,3 +4875,4,150030,3 +4876,4,150040,3 +4878,3,107000060,1 +4879,3,107000070,1 +4880,3,107000080,1 +4881,3,108000050,1 +4882,3,109000050,1 +4883,4,107000100,1 +4884,4,103000090,1 +4885,5,107000110,1 +4886,1,105000000,2 +4887,2,105000001,2 +4888,3,105000002,2 +4889,4,105000003,2 +4890,5,105000004,2 +4891,1,120000000,2 +4892,2,120000001,2 +4893,3,120000002,2 +4894,4,120000003,2 +4895,5,120000004,2 +4896,3,180004,3 +4897,4,180005,3 +4899,3,106000060,1 +4900,3,106000070,1 +4901,3,106000080,1 +4902,3,101000070,1 +4903,3,110000060,1 +4904,4,106000100,1 +4905,4,106000110,1 +4906,4,104000090,1 +4907,5,106000120,1 +4908,1,103000000,2 +4909,2,103000001,2 +4910,3,103000002,2 +4911,4,103000003,2 +4912,5,103000004,2 +4913,1,112000000,2 +4914,2,112000001,2 +4915,3,112000002,2 +4916,4,112000003,2 +4917,5,112000004,2 +4918,3,170004,3 +4919,4,170005,3 +4921,3,112000020,1 +4922,3,112000030,1 +4923,4,112000040,1 +4924,5,112000060,1 +4925,1,101000000,2 +4926,2,101000001,2 +4927,3,101000002,2 +4928,4,101000008,2 +4929,5,101000004,2 +4930,1,112000000,2 +4931,2,112000001,2 +4932,3,112000002,2 +4933,4,112000003,2 +4934,5,112000004,2 +4935,3,170004,3 +4936,4,170005,3 +4938,3,104000170,1 +4939,4,104000180,1 +4940,5,104000190,1 +4941,1,115000000,2 +4942,2,115000001,2 +4943,3,115000002,2 +4944,4,115000003,2 +4945,5,115000004,2 +4946,1,120000000,2 +4947,2,120000001,2 +4948,3,120000002,2 +4949,4,120000003,2 +4950,5,120000004,2 +4951,4,120024,3 +4952,4,120034,3 +4953,4,120044,3 +4954,4,120054,3 +4955,3,120241,3 +4956,3,120251,3 +4957,3,120261,3 +4958,3,120271,3 +4959,3,120300,3 +4960,3,120310,3 +4961,3,120320,3 +4962,3,120330,3 +4963,3,120340,3 +4964,3,120350,3 +4965,3,120360,3 +4966,3,120370,3 +4967,3,120380,3 +4968,3,120390,3 +4969,3,120400,3 +4970,3,120410,3 +4971,3,120420,3 +4972,3,120430,3 +4973,3,120450,3 +4974,3,120460,3 +4975,3,120550,3 +4976,3,120560,3 +4977,3,120570,3 +4978,3,120990,3 +4979,3,121000,3 +4980,3,121010,3 +4981,3,121020,3 +4982,4,140000,3 +4983,4,150010,3 +4984,4,150020,3 +4985,4,150030,3 +4986,4,150040,3 +4988,3,111000010,1 +4989,3,111000020,1 +4990,3,111000030,1 +4991,4,111000040,1 +4992,5,111000060,1 +4993,1,101000000,2 +4994,2,101000001,2 +4995,3,101000002,2 +4996,4,101000008,2 +4997,5,101000004,2 +4998,1,120000000,2 +4999,2,120000001,2 +5000,3,120000002,2 +5001,4,120000003,2 +5002,5,120000004,2 +5003,4,110024,3 +5004,4,110034,3 +5005,4,110044,3 +5006,4,110054,3 +5007,3,110060,3 +5008,3,110070,3 +5009,3,110080,3 +5010,3,110090,3 +5011,3,110100,3 +5012,3,110110,3 +5013,3,110120,3 +5014,3,110130,3 +5015,3,110140,3 +5016,3,110150,3 +5017,3,110160,3 +5018,3,110170,3 +5019,3,110180,3 +5020,3,110190,3 +5021,3,110200,3 +5022,3,110210,3 +5023,3,110220,3 +5024,3,110230,3 +5025,3,110240,3 +5026,3,110250,3 +5027,3,110260,3 +5028,3,110270,3 +5029,3,110620,3 +5030,3,110670,3 +5031,4,140000,3 +5032,4,150010,3 +5033,4,150020,3 +5034,4,150030,3 +5035,4,150040,3 +5037,3,108000060,1 +5038,3,108000070,1 +5039,3,108000080,1 +5040,3,107000050,1 +5041,3,112000010,1 +5042,4,108000100,1 +5043,4,105000090,1 +5044,5,108000110,1 +5045,1,108000000,2 +5046,2,108000001,2 +5047,3,108000002,2 +5048,4,108000003,2 +5049,5,108000004,2 +5050,1,120000000,2 +5051,2,120000001,2 +5052,3,120000002,2 +5053,4,120000003,2 +5054,5,120000004,2 +5055,4,130024,3 +5056,4,130034,3 +5057,4,130044,3 +5058,4,130054,3 +5059,3,130060,3 +5060,3,130070,3 +5061,3,130080,3 +5062,3,130090,3 +5063,3,130100,3 +5064,3,130110,3 +5065,3,130120,3 +5066,3,130130,3 +5067,3,130140,3 +5068,3,130150,3 +5069,3,130160,3 +5070,3,130170,3 +5071,3,130180,3 +5072,3,130190,3 +5073,3,130200,3 +5074,3,130420,3 +5075,3,130510,3 +5076,3,130520,3 +5077,3,130530,3 +5078,3,130540,3 +5079,3,130660,3 +5080,4,140000,3 +5081,4,150010,3 +5082,4,150020,3 +5083,4,150030,3 +5084,4,150040,3 +5086,3,111000010,1 +5087,3,111000020,1 +5088,3,111000030,1 +5089,4,111000040,1 +5090,5,111000060,1 +5091,1,170002,3 +5092,2,170003,3 +5093,3,170004,3 +5094,1,180002,3 +5095,2,180003,3 +5096,3,180004,3 +5097,1,201000010,8 +5098,1,292000010,8 +5099,1,299000040,8 +5100,1,299000070,8 +5101,1,299000110,8 +5102,1,299000120,8 +5103,1,299000140,8 +5104,2,202000010,8 +5105,2,290000010,8 +5106,2,299000010,8 +5107,2,299000150,8 +5108,2,299000190,8 +5109,2,299000200,8 +5110,2,299000210,8 +5111,3,298000050,8 +5112,3,298000060,8 +5113,3,299000060,8 +5114,3,299000170,8 +5115,4,140000,3 +5116,5,150010,3 +5117,5,150020,3 +5118,5,150030,3 +5119,5,150040,3 +5121,3,111000010,1 +5122,3,111000020,1 +5123,3,111000030,1 +5124,4,111000040,1 +5125,5,111000060,1 +5126,2,170003,3 +5127,3,170004,3 +5128,2,180003,3 +5129,3,180004,3 +5130,1,201000010,8 +5131,1,292000010,8 +5132,1,299000040,8 +5133,1,299000070,8 +5134,1,299000110,8 +5135,1,299000120,8 +5136,1,299000140,8 +5137,2,202000010,8 +5138,2,290000010,8 +5139,2,299000010,8 +5140,2,299000150,8 +5141,2,299000190,8 +5142,2,299000200,8 +5143,2,299000210,8 +5144,3,298000050,8 +5145,3,298000060,8 +5146,3,299000060,8 +5147,3,299000170,8 +5148,4,140000,3 +5149,5,150010,3 +5150,5,150020,3 +5151,5,150030,3 +5152,5,150040,3 +5154,3,111000010,1 +5155,3,111000020,1 +5156,3,111000030,1 +5157,4,111000040,1 +5158,5,111000060,1 +5159,3,170004,3 +5160,3,180004,3 +5161,1,201000010,8 +5162,1,292000010,8 +5163,1,299000040,8 +5164,1,299000070,8 +5165,1,299000110,8 +5166,1,299000120,8 +5167,1,299000140,8 +5168,2,202000010,8 +5169,2,290000010,8 +5170,2,299000010,8 +5171,2,299000150,8 +5172,2,299000190,8 +5173,2,299000200,8 +5174,2,299000210,8 +5175,3,298000050,8 +5176,3,298000060,8 +5177,3,299000060,8 +5178,3,299000170,8 +5179,4,140000,3 +5180,5,150010,3 +5181,5,150020,3 +5182,5,150030,3 +5183,5,150040,3 +5185,3,111000010,1 +5186,3,111000020,1 +5187,3,111000030,1 +5188,4,111000040,1 +5189,5,111000060,1 +5190,3,170004,3 +5191,3,180004,3 +5192,1,201000010,8 +5193,1,292000010,8 +5194,1,299000040,8 +5195,1,299000070,8 +5196,1,299000110,8 +5197,1,299000120,8 +5198,1,299000140,8 +5199,2,202000010,8 +5200,2,290000010,8 +5201,2,299000010,8 +5202,2,299000150,8 +5203,2,299000190,8 +5204,2,299000200,8 +5205,2,299000210,8 +5206,3,298000050,8 +5207,3,298000060,8 +5208,3,299000060,8 +5209,3,299000170,8 +5210,4,140000,3 +5211,5,150010,3 +5212,5,150020,3 +5213,5,150030,3 +5214,5,150040,3 +5216,3,111000010,1 +5217,3,111000020,1 +5218,3,111000030,1 +5219,4,111000040,1 +5220,5,111000060,1 +5221,3,170004,3 +5222,3,180004,3 +5223,1,201000010,8 +5224,1,292000010,8 +5225,1,299000040,8 +5226,1,299000070,8 +5227,1,299000110,8 +5228,1,299000120,8 +5229,1,299000140,8 +5230,2,202000010,8 +5231,2,290000010,8 +5232,2,299000010,8 +5233,2,299000150,8 +5234,2,299000190,8 +5235,2,299000200,8 +5236,2,299000210,8 +5237,3,298000050,8 +5238,3,298000060,8 +5239,3,299000060,8 +5240,3,299000170,8 +5241,4,140000,3 +5242,5,150010,3 +5243,5,150020,3 +5244,5,150030,3 +5245,5,150040,3 +5246,5,190000,3 +5247,5,200000,3 +5248,5,210000,3 +5250,3,101000050,1 +5251,3,101000060,1 +5252,3,101000080,1 +5253,3,101000040,1 +5254,3,109000060,1 +5255,3,109000070,1 +5256,3,109000080,1 +5257,3,105000060,1 +5258,3,105000070,1 +5259,3,105000080,1 +5260,3,104000050,1 +5261,3,106000050,1 +5262,4,101000090,1 +5263,4,101000100,1 +5264,4,101000110,1 +5265,4,109000100,1 +5266,4,105000100,1 +5267,4,105000110,1 +5268,4,108000090,1 +5269,4,110000090,1 +5270,5,101000120,1 +5271,5,109000110,1 +5272,5,105000120,1 +5273,1,101000000,2 +5274,2,101000001,2 +5275,3,101000002,2 +5276,4,101000008,2 +5277,5,101000004,2 +5278,1,109000000,2 +5279,2,109000001,2 +5280,3,109000002,2 +5281,4,109000003,2 +5282,5,109000004,2 +5283,3,170004,3 +5284,4,170005,3 +5285,3,180004,3 +5286,4,180005,3 +5287,4,140000,3 +5288,4,150010,3 +5289,4,150020,3 +5290,4,150030,3 +5291,4,150040,3 +5293,3,102000060,1 +5294,3,102000070,1 +5295,3,102000080,1 +5296,3,103000050,1 +5297,3,105000050,1 +5298,3,107000060,1 +5299,3,107000070,1 +5300,3,107000080,1 +5301,3,108000050,1 +5302,3,109000050,1 +5303,3,103000060,1 +5304,3,103000070,1 +5305,3,103000080,1 +5306,3,110000050,1 +5307,4,102000100,1 +5308,4,102000110,1 +5309,4,106000090,1 +5310,4,109000090,1 +5311,4,107000100,1 +5312,4,103000090,1 +5313,4,102000090,1 +5314,4,103000100,1 +5315,5,102000120,1 +5316,5,107000110,1 +5317,5,103000120,1 +5318,1,102000000,2 +5319,2,102000001,2 +5320,3,102000002,2 +5321,4,102000003,2 +5322,5,102000004,2 +5323,1,105000000,2 +5324,2,105000001,2 +5325,3,105000002,2 +5326,4,105000003,2 +5327,5,105000004,2 +5328,1,112000000,2 +5329,2,112000001,2 +5330,3,112000002,2 +5331,4,112000003,2 +5332,5,112000004,2 +5333,4,110024,3 +5334,4,110034,3 +5335,4,110044,3 +5336,4,110054,3 +5337,3,110060,3 +5338,3,110070,3 +5339,3,110080,3 +5340,3,110090,3 +5341,3,110100,3 +5342,3,110110,3 +5343,3,110120,3 +5344,3,110130,3 +5345,3,110140,3 +5346,3,110150,3 +5347,3,110160,3 +5348,3,110170,3 +5349,3,110180,3 +5350,3,110190,3 +5351,3,110200,3 +5352,3,110210,3 +5353,3,110220,3 +5354,3,110230,3 +5355,3,110240,3 +5356,3,110250,3 +5357,3,110260,3 +5358,3,110270,3 +5359,3,110620,3 +5360,3,110670,3 +5361,4,140000,3 +5362,4,150010,3 +5363,4,150020,3 +5364,4,150030,3 +5365,4,150040,3 +5367,3,106000060,1 +5368,3,106000070,1 +5369,3,106000080,1 +5370,3,101000070,1 +5371,3,110000060,1 +5372,3,104000060,1 +5373,3,104000070,1 +5374,3,104000080,1 +5375,3,102000050,1 +5376,3,104000170,1 +5377,3,104000260,1 +5378,4,106000100,1 +5379,4,106000110,1 +5380,4,104000090,1 +5381,4,104000100,1 +5382,4,104000110,1 +5383,4,107000090,1 +5384,4,104000180,1 +5385,4,104000270,1 +5386,5,106000120,1 +5387,5,104000120,1 +5388,5,104000190,1 +5389,1,103000000,2 +5390,2,103000001,2 +5391,3,103000002,2 +5392,4,103000003,2 +5393,5,103000004,2 +5394,1,111000000,2 +5395,2,111000001,2 +5396,3,111000002,2 +5397,4,111000003,2 +5398,5,111000004,2 +5399,1,115000000,2 +5400,2,115000001,2 +5401,3,115000002,2 +5402,4,115000003,2 +5403,5,115000004,2 +5404,4,120024,3 +5405,4,120034,3 +5406,4,120044,3 +5407,4,120054,3 +5408,3,120241,3 +5409,3,120251,3 +5410,3,120261,3 +5411,3,120271,3 +5412,3,120300,3 +5413,3,120310,3 +5414,3,120320,3 +5415,3,120330,3 +5416,3,120340,3 +5417,3,120350,3 +5418,3,120360,3 +5419,3,120370,3 +5420,3,120380,3 +5421,3,120390,3 +5422,3,120400,3 +5423,3,120410,3 +5424,3,120420,3 +5425,3,120430,3 +5426,3,120450,3 +5427,3,120460,3 +5428,3,120550,3 +5429,3,120560,3 +5430,3,120570,3 +5431,3,120990,3 +5432,3,121000,3 +5433,3,121010,3 +5434,3,121020,3 +5435,4,140000,3 +5436,4,150010,3 +5437,4,150020,3 +5438,4,150030,3 +5439,4,150040,3 +5441,3,111000010,1 +5442,3,111000020,1 +5443,3,111000030,1 +5444,3,112000020,1 +5445,3,112000030,1 +5446,3,108000060,1 +5447,3,108000070,1 +5448,3,108000080,1 +5449,3,107000050,1 +5450,3,112000010,1 +5451,3,110000070,1 +5452,3,110000080,1 +5453,4,111000040,1 +5454,4,112000040,1 +5455,4,108000100,1 +5456,4,105000090,1 +5457,4,110000100,1 +5458,5,111000060,1 +5459,5,112000060,1 +5460,5,108000110,1 +5461,5,110000110,1 +5462,1,108000000,2 +5463,2,108000001,2 +5464,3,108000002,2 +5465,4,108000003,2 +5466,5,108000004,2 +5467,1,107000000,2 +5468,2,107000001,2 +5469,3,107000002,2 +5470,4,107000003,2 +5471,5,107000004,2 +5472,1,120000000,2 +5473,2,120000001,2 +5474,3,120000002,2 +5475,4,120000003,2 +5476,5,120000004,2 +5477,4,130024,3 +5478,4,130034,3 +5479,4,130044,3 +5480,4,130054,3 +5481,3,130060,3 +5482,3,130070,3 +5483,3,130080,3 +5484,3,130090,3 +5485,3,130100,3 +5486,3,130110,3 +5487,3,130120,3 +5488,3,130130,3 +5489,3,130140,3 +5490,3,130150,3 +5491,3,130160,3 +5492,3,130170,3 +5493,3,130180,3 +5494,3,130190,3 +5495,3,130200,3 +5496,3,130420,3 +5497,3,130510,3 +5498,3,130520,3 +5499,3,130530,3 +5500,3,130540,3 +5501,3,130660,3 +5502,4,140000,3 +5503,4,150010,3 +5504,4,150020,3 +5505,4,150030,3 +5506,4,150040,3 +5508,1,101000000,2 +5509,2,101000001,2 +5510,3,101000002,2 +5511,4,101000008,2 +5512,5,101000004,2 +5513,1,120000000,2 +5514,2,120000001,2 +5515,3,120000002,2 +5516,4,120000003,2 +5517,5,120000004,2 +5519,1,101000010,1 +5520,1,102000010,1 +5521,1,103000010,1 +5522,1,104000010,1 +5523,1,105000010,1 +5524,1,106000010,1 +5525,1,107000010,1 +5526,1,108000010,1 +5527,1,109000010,1 +5528,1,110000010,1 +5529,2,101000020,1 +5530,2,101000030,1 +5531,2,102000020,1 +5532,2,102000030,1 +5533,2,102000040,1 +5534,2,103000020,1 +5535,2,103000030,1 +5536,2,103000040,1 +5537,2,104000020,1 +5538,2,104000030,1 +5539,2,104000040,1 +5540,2,105000020,1 +5541,2,105000030,1 +5542,2,105000040,1 +5543,2,106000020,1 +5544,2,106000030,1 +5545,2,106000040,1 +5546,2,107000020,1 +5547,2,107000030,1 +5548,2,107000040,1 +5549,2,108000020,1 +5550,2,108000030,1 +5551,2,108000040,1 +5552,2,109000020,1 +5553,2,109000030,1 +5554,2,109000040,1 +5555,2,110000020,1 +5556,2,110000030,1 +5557,2,110000040,1 +5558,3,101000050,1 +5559,3,101000060,1 +5560,3,101000080,1 +5561,3,101000040,1 +5562,3,109000060,1 +5563,3,109000070,1 +5564,3,109000080,1 +5565,3,105000060,1 +5566,3,105000070,1 +5567,3,105000080,1 +5568,3,104000050,1 +5569,3,106000050,1 +5570,3,102000060,1 +5571,3,102000070,1 +5572,3,102000080,1 +5573,3,103000050,1 +5574,3,105000050,1 +5575,3,107000060,1 +5576,3,107000070,1 +5577,3,107000080,1 +5578,3,108000050,1 +5579,3,109000050,1 +5580,3,103000060,1 +5581,3,103000070,1 +5582,3,103000080,1 +5583,3,110000050,1 +5584,3,106000060,1 +5585,3,106000070,1 +5586,3,106000080,1 +5587,3,101000070,1 +5588,3,110000060,1 +5589,3,104000060,1 +5590,3,104000070,1 +5591,3,104000080,1 +5592,3,102000050,1 +5593,3,104000170,1 +5594,3,104000260,1 +5595,3,111000010,1 +5596,3,111000020,1 +5597,3,111000030,1 +5598,3,112000020,1 +5599,3,112000030,1 +5600,3,108000060,1 +5601,3,108000070,1 +5602,3,108000080,1 +5603,3,107000050,1 +5604,3,112000010,1 +5605,3,110000070,1 +5606,3,110000080,1 +5607,4,101000090,1 +5608,4,101000100,1 +5609,4,101000110,1 +5610,4,109000100,1 +5611,4,105000100,1 +5612,4,105000110,1 +5613,4,108000090,1 +5614,4,110000090,1 +5615,4,102000100,1 +5616,4,102000110,1 +5617,4,106000090,1 +5618,4,109000090,1 +5619,4,107000100,1 +5620,4,103000090,1 +5621,4,102000090,1 +5622,4,103000100,1 +5623,4,106000100,1 +5624,4,106000110,1 +5625,4,104000090,1 +5626,4,104000100,1 +5627,4,104000110,1 +5628,4,107000090,1 +5629,4,104000180,1 +5630,4,111000040,1 +5631,4,112000040,1 +5632,4,108000100,1 +5633,4,105000090,1 +5634,4,110000100,1 +5635,5,101000120,1 +5636,5,109000110,1 +5637,5,105000120,1 +5638,5,102000120,1 +5639,5,107000110,1 +5640,5,103000120,1 +5641,5,106000120,1 +5642,5,104000120,1 +5643,5,104000190,1 +5644,5,111000060,1 +5645,5,112000060,1 +5646,5,108000110,1 +5647,5,110000110,1 +5648,1,170002,3 +5649,2,170003,3 +5650,3,170004,3 +5651,1,180002,3 +5652,2,180003,3 +5653,3,180004,3 +5654,1,201000010,8 +5655,1,292000010,8 +5656,1,299000040,8 +5657,1,299000070,8 +5658,1,299000110,8 +5659,1,299000120,8 +5660,1,299000140,8 +5661,2,202000010,8 +5662,2,290000010,8 +5663,2,299000010,8 +5664,2,299000150,8 +5665,2,299000190,8 +5666,2,299000200,8 +5667,2,299000210,8 +5668,3,298000050,8 +5669,3,298000060,8 +5670,3,299000060,8 +5671,3,299000170,8 +5672,5,297000100,8 +5673,5,291000020,8 +5674,4,140000,3 +5675,5,150010,3 +5676,5,150020,3 +5677,5,150030,3 +5678,5,150040,3 +5680,1,101000010,1 +5681,1,102000010,1 +5682,1,103000010,1 +5683,1,104000010,1 +5684,1,105000010,1 +5685,1,106000010,1 +5686,1,107000010,1 +5687,1,108000010,1 +5688,1,109000010,1 +5689,1,110000010,1 +5690,2,101000020,1 +5691,2,101000030,1 +5692,2,102000020,1 +5693,2,102000030,1 +5694,2,102000040,1 +5695,2,103000020,1 +5696,2,103000030,1 +5697,2,103000040,1 +5698,2,104000020,1 +5699,2,104000030,1 +5700,2,104000040,1 +5701,2,105000020,1 +5702,2,105000030,1 +5703,2,105000040,1 +5704,2,106000020,1 +5705,2,106000030,1 +5706,2,106000040,1 +5707,2,107000020,1 +5708,2,107000030,1 +5709,2,107000040,1 +5710,2,108000020,1 +5711,2,108000030,1 +5712,2,108000040,1 +5713,2,109000020,1 +5714,2,109000030,1 +5715,2,109000040,1 +5716,2,110000020,1 +5717,2,110000030,1 +5718,2,110000040,1 +5719,3,101000050,1 +5720,3,101000060,1 +5721,3,101000080,1 +5722,3,101000040,1 +5723,3,109000060,1 +5724,3,109000070,1 +5725,3,109000080,1 +5726,3,105000060,1 +5727,3,105000070,1 +5728,3,105000080,1 +5729,3,104000050,1 +5730,3,106000050,1 +5731,3,102000060,1 +5732,3,102000070,1 +5733,3,102000080,1 +5734,3,103000050,1 +5735,3,105000050,1 +5736,3,107000060,1 +5737,3,107000070,1 +5738,3,107000080,1 +5739,3,108000050,1 +5740,3,109000050,1 +5741,3,103000060,1 +5742,3,103000070,1 +5743,3,103000080,1 +5744,3,110000050,1 +5745,3,106000060,1 +5746,3,106000070,1 +5747,3,106000080,1 +5748,3,101000070,1 +5749,3,110000060,1 +5750,3,104000060,1 +5751,3,104000070,1 +5752,3,104000080,1 +5753,3,102000050,1 +5754,3,104000170,1 +5755,3,104000260,1 +5756,3,111000010,1 +5757,3,111000020,1 +5758,3,111000030,1 +5759,3,112000020,1 +5760,3,112000030,1 +5761,3,108000060,1 +5762,3,108000070,1 +5763,3,108000080,1 +5764,3,107000050,1 +5765,3,112000010,1 +5766,3,110000070,1 +5767,3,110000080,1 +5768,4,101000090,1 +5769,4,101000100,1 +5770,4,101000110,1 +5771,4,109000100,1 +5772,4,105000100,1 +5773,4,105000110,1 +5774,4,108000090,1 +5775,4,110000090,1 +5776,4,102000100,1 +5777,4,102000110,1 +5778,4,106000090,1 +5779,4,109000090,1 +5780,4,107000100,1 +5781,4,103000090,1 +5782,4,102000090,1 +5783,4,103000100,1 +5784,4,106000100,1 +5785,4,106000110,1 +5786,4,104000090,1 +5787,4,104000100,1 +5788,4,104000110,1 +5789,4,107000090,1 +5790,4,104000180,1 +5791,4,111000040,1 +5792,4,112000040,1 +5793,4,108000100,1 +5794,4,105000090,1 +5795,4,110000100,1 +5796,5,101000120,1 +5797,5,109000110,1 +5798,5,105000120,1 +5799,5,102000120,1 +5800,5,107000110,1 +5801,5,103000120,1 +5802,5,106000120,1 +5803,5,104000120,1 +5804,5,104000190,1 +5805,5,111000060,1 +5806,5,112000060,1 +5807,5,108000110,1 +5808,5,110000110,1 +5809,2,170003,3 +5810,3,170004,3 +5811,2,180003,3 +5812,3,180004,3 +5813,1,201000010,8 +5814,1,292000010,8 +5815,1,299000040,8 +5816,1,299000070,8 +5817,1,299000110,8 +5818,1,299000120,8 +5819,1,299000140,8 +5820,2,202000010,8 +5821,2,290000010,8 +5822,2,299000010,8 +5823,2,299000150,8 +5824,2,299000190,8 +5825,2,299000200,8 +5826,2,299000210,8 +5827,3,298000050,8 +5828,3,298000060,8 +5829,3,299000060,8 +5830,3,299000170,8 +5831,5,297000100,8 +5832,5,291000020,8 +5833,4,140000,3 +5834,5,150010,3 +5835,5,150020,3 +5836,5,150030,3 +5837,5,150040,3 +5839,3,101000050,1 +5840,3,101000060,1 +5841,3,101000080,1 +5842,3,101000040,1 +5843,3,109000060,1 +5844,3,109000070,1 +5845,3,109000080,1 +5846,3,105000060,1 +5847,3,105000070,1 +5848,3,105000080,1 +5849,3,104000050,1 +5850,3,106000050,1 +5851,3,102000060,1 +5852,3,102000070,1 +5853,3,102000080,1 +5854,3,103000050,1 +5855,3,105000050,1 +5856,3,107000060,1 +5857,3,107000070,1 +5858,3,107000080,1 +5859,3,108000050,1 +5860,3,109000050,1 +5861,3,103000060,1 +5862,3,103000070,1 +5863,3,103000080,1 +5864,3,110000050,1 +5865,3,106000060,1 +5866,3,106000070,1 +5867,3,106000080,1 +5868,3,101000070,1 +5869,3,110000060,1 +5870,3,104000060,1 +5871,3,104000070,1 +5872,3,104000080,1 +5873,3,102000050,1 +5874,3,104000170,1 +5875,3,104000260,1 +5876,3,111000010,1 +5877,3,111000020,1 +5878,3,111000030,1 +5879,3,112000020,1 +5880,3,112000030,1 +5881,3,108000060,1 +5882,3,108000070,1 +5883,3,108000080,1 +5884,3,107000050,1 +5885,3,112000010,1 +5886,3,110000070,1 +5887,3,110000080,1 +5888,4,101000090,1 +5889,4,101000100,1 +5890,4,101000110,1 +5891,4,109000100,1 +5892,4,105000100,1 +5893,4,105000110,1 +5894,4,108000090,1 +5895,4,110000090,1 +5896,4,102000100,1 +5897,4,102000110,1 +5898,4,106000090,1 +5899,4,109000090,1 +5900,4,107000100,1 +5901,4,103000090,1 +5902,4,102000090,1 +5903,4,103000100,1 +5904,4,106000100,1 +5905,4,106000110,1 +5906,4,104000090,1 +5907,4,104000100,1 +5908,4,104000110,1 +5909,4,107000090,1 +5910,4,104000180,1 +5911,4,111000040,1 +5912,4,112000040,1 +5913,4,108000100,1 +5914,4,105000090,1 +5915,4,110000100,1 +5916,5,101000120,1 +5917,5,109000110,1 +5918,5,105000120,1 +5919,5,102000120,1 +5920,5,107000110,1 +5921,5,103000120,1 +5922,5,106000120,1 +5923,5,104000120,1 +5924,5,104000190,1 +5925,5,111000060,1 +5926,5,112000060,1 +5927,5,108000110,1 +5928,5,110000110,1 +5929,3,170004,3 +5930,3,180004,3 +5931,1,201000010,8 +5932,1,292000010,8 +5933,1,299000040,8 +5934,1,299000070,8 +5935,1,299000110,8 +5936,1,299000120,8 +5937,1,299000140,8 +5938,2,202000010,8 +5939,2,290000010,8 +5940,2,299000010,8 +5941,2,299000150,8 +5942,2,299000190,8 +5943,2,299000200,8 +5944,2,299000210,8 +5945,3,298000050,8 +5946,3,298000060,8 +5947,3,299000060,8 +5948,3,299000170,8 +5949,5,297000100,8 +5950,5,291000020,8 +5951,4,140000,3 +5952,5,150010,3 +5953,5,150020,3 +5954,5,150030,3 +5955,5,150040,3 +5957,3,101000050,1 +5958,3,101000060,1 +5959,3,101000080,1 +5960,3,101000040,1 +5961,3,109000060,1 +5962,3,109000070,1 +5963,3,109000080,1 +5964,3,105000060,1 +5965,3,105000070,1 +5966,3,105000080,1 +5967,3,104000050,1 +5968,3,106000050,1 +5969,3,102000060,1 +5970,3,102000070,1 +5971,3,102000080,1 +5972,3,103000050,1 +5973,3,105000050,1 +5974,3,107000060,1 +5975,3,107000070,1 +5976,3,107000080,1 +5977,3,108000050,1 +5978,3,109000050,1 +5979,3,103000060,1 +5980,3,103000070,1 +5981,3,103000080,1 +5982,3,110000050,1 +5983,3,106000060,1 +5984,3,106000070,1 +5985,3,106000080,1 +5986,3,101000070,1 +5987,3,110000060,1 +5988,3,104000060,1 +5989,3,104000070,1 +5990,3,104000080,1 +5991,3,102000050,1 +5992,3,104000170,1 +5993,3,104000260,1 +5994,3,111000010,1 +5995,3,111000020,1 +5996,3,111000030,1 +5997,3,112000020,1 +5998,3,112000030,1 +5999,3,108000060,1 +6000,3,108000070,1 +6001,3,108000080,1 +6002,3,107000050,1 +6003,3,112000010,1 +6004,3,110000070,1 +6005,3,110000080,1 +6006,4,101000090,1 +6007,4,101000100,1 +6008,4,101000110,1 +6009,4,109000100,1 +6010,4,105000100,1 +6011,4,105000110,1 +6012,4,108000090,1 +6013,4,110000090,1 +6014,4,102000100,1 +6015,4,102000110,1 +6016,4,106000090,1 +6017,4,109000090,1 +6018,4,107000100,1 +6019,4,103000090,1 +6020,4,102000090,1 +6021,4,103000100,1 +6022,4,106000100,1 +6023,4,106000110,1 +6024,4,104000090,1 +6025,4,104000100,1 +6026,4,104000110,1 +6027,4,107000090,1 +6028,4,104000180,1 +6029,4,111000040,1 +6030,4,112000040,1 +6031,4,108000100,1 +6032,4,105000090,1 +6033,4,110000100,1 +6034,5,101000120,1 +6035,5,109000110,1 +6036,5,105000120,1 +6037,5,102000120,1 +6038,5,107000110,1 +6039,5,103000120,1 +6040,5,106000120,1 +6041,5,104000120,1 +6042,5,104000190,1 +6043,5,111000060,1 +6044,5,112000060,1 +6045,5,108000110,1 +6046,5,110000110,1 +6047,3,170004,3 +6048,3,180004,3 +6049,1,201000010,8 +6050,1,292000010,8 +6051,1,299000040,8 +6052,1,299000070,8 +6053,1,299000110,8 +6054,1,299000120,8 +6055,1,299000140,8 +6056,2,202000010,8 +6057,2,290000010,8 +6058,2,299000010,8 +6059,2,299000150,8 +6060,2,299000190,8 +6061,2,299000200,8 +6062,2,299000210,8 +6063,3,298000050,8 +6064,3,298000060,8 +6065,3,299000060,8 +6066,3,299000170,8 +6067,5,297000100,8 +6068,5,291000020,8 +6069,4,140000,3 +6070,5,150010,3 +6071,5,150020,3 +6072,5,150030,3 +6073,5,150040,3 +6075,3,101000050,1 +6076,3,101000060,1 +6077,3,101000080,1 +6078,3,101000040,1 +6079,3,109000060,1 +6080,3,109000070,1 +6081,3,109000080,1 +6082,3,105000060,1 +6083,3,105000070,1 +6084,3,105000080,1 +6085,3,104000050,1 +6086,3,106000050,1 +6087,3,102000060,1 +6088,3,102000070,1 +6089,3,102000080,1 +6090,3,103000050,1 +6091,3,105000050,1 +6092,3,107000060,1 +6093,3,107000070,1 +6094,3,107000080,1 +6095,3,108000050,1 +6096,3,109000050,1 +6097,3,103000060,1 +6098,3,103000070,1 +6099,3,103000080,1 +6100,3,110000050,1 +6101,3,106000060,1 +6102,3,106000070,1 +6103,3,106000080,1 +6104,3,101000070,1 +6105,3,110000060,1 +6106,3,104000060,1 +6107,3,104000070,1 +6108,3,104000080,1 +6109,3,102000050,1 +6110,3,104000170,1 +6111,3,104000260,1 +6112,3,111000010,1 +6113,3,111000020,1 +6114,3,111000030,1 +6115,3,112000020,1 +6116,3,112000030,1 +6117,3,108000060,1 +6118,3,108000070,1 +6119,3,108000080,1 +6120,3,107000050,1 +6121,3,112000010,1 +6122,3,110000070,1 +6123,3,110000080,1 +6124,4,101000090,1 +6125,4,101000100,1 +6126,4,101000110,1 +6127,4,109000100,1 +6128,4,105000100,1 +6129,4,105000110,1 +6130,4,108000090,1 +6131,4,110000090,1 +6132,4,102000100,1 +6133,4,102000110,1 +6134,4,106000090,1 +6135,4,109000090,1 +6136,4,107000100,1 +6137,4,103000090,1 +6138,4,102000090,1 +6139,4,103000100,1 +6140,4,106000100,1 +6141,4,106000110,1 +6142,4,104000090,1 +6143,4,104000100,1 +6144,4,104000110,1 +6145,4,107000090,1 +6146,4,104000180,1 +6147,4,111000040,1 +6148,4,112000040,1 +6149,4,108000100,1 +6150,4,105000090,1 +6151,4,110000100,1 +6152,5,101000120,1 +6153,5,109000110,1 +6154,5,105000120,1 +6155,5,102000120,1 +6156,5,107000110,1 +6157,5,103000120,1 +6158,5,106000120,1 +6159,5,104000120,1 +6160,5,104000190,1 +6161,5,111000060,1 +6162,5,112000060,1 +6163,5,108000110,1 +6164,5,110000110,1 +6165,3,170004,3 +6166,3,180004,3 +6167,1,201000010,8 +6168,1,292000010,8 +6169,1,299000040,8 +6170,1,299000070,8 +6171,1,299000110,8 +6172,1,299000120,8 +6173,1,299000140,8 +6174,2,202000010,8 +6175,2,290000010,8 +6176,2,299000010,8 +6177,2,299000150,8 +6178,2,299000190,8 +6179,2,299000200,8 +6180,2,299000210,8 +6181,3,298000050,8 +6182,3,298000060,8 +6183,3,299000060,8 +6184,3,299000170,8 +6185,5,297000100,8 +6186,5,291000020,8 +6187,4,140000,3 +6188,5,150010,3 +6189,5,150020,3 +6190,5,150030,3 +6191,5,150040,3 +6192,5,190000,3 +6193,5,200000,3 +6194,5,210000,3 +6196,3,111000010,1 +6197,3,111000020,1 +6198,3,111000030,1 +6199,3,112000020,1 +6200,3,112000030,1 +6201,3,108000060,1 +6202,3,108000070,1 +6203,3,108000080,1 +6204,3,107000050,1 +6205,3,112000010,1 +6206,3,110000070,1 +6207,3,110000080,1 +6208,4,111000040,1 +6209,4,112000040,1 +6210,4,108000100,1 +6211,4,105000090,1 +6212,4,110000100,1 +6213,5,111000060,1 +6214,5,112000060,1 +6215,5,108000110,1 +6216,5,110000110,1 +6217,1,108000000,2 +6218,2,108000001,2 +6219,3,108000002,2 +6220,4,108000003,2 +6221,5,108000004,2 +6222,1,107000000,2 +6223,2,107000001,2 +6224,3,107000002,2 +6225,4,107000003,2 +6226,5,107000004,2 +6227,1,120000000,2 +6228,2,120000001,2 +6229,3,120000002,2 +6230,4,120000003,2 +6231,5,120000004,2 +6232,3,170004,3 +6233,4,170005,3 +6234,3,180004,3 +6235,4,180005,3 +6236,4,140000,3 +6237,4,150010,3 +6238,4,150020,3 +6239,4,150030,3 +6240,4,150040,3 +6242,3,101000050,1 +6243,3,101000060,1 +6244,3,101000080,1 +6245,3,101000040,1 +6246,3,109000060,1 +6247,3,109000070,1 +6248,3,109000080,1 +6249,3,105000060,1 +6250,3,105000070,1 +6251,3,105000080,1 +6252,3,104000050,1 +6253,3,106000050,1 +6254,4,101000090,1 +6255,4,101000100,1 +6256,4,101000110,1 +6257,4,109000100,1 +6258,4,105000100,1 +6259,4,105000110,1 +6260,4,108000090,1 +6261,4,110000090,1 +6262,5,101000120,1 +6263,5,109000110,1 +6264,5,105000120,1 +6265,1,101000000,2 +6266,2,101000001,2 +6267,3,101000002,2 +6268,4,101000008,2 +6269,5,101000004,2 +6270,1,109000000,2 +6271,2,109000001,2 +6272,3,109000002,2 +6273,4,109000003,2 +6274,5,109000004,2 +6275,4,110024,3 +6276,4,110034,3 +6277,4,110044,3 +6278,4,110054,3 +6279,3,110060,3 +6280,3,110070,3 +6281,3,110080,3 +6282,3,110090,3 +6283,3,110100,3 +6284,3,110110,3 +6285,3,110120,3 +6286,3,110130,3 +6287,3,110140,3 +6288,3,110150,3 +6289,3,110160,3 +6290,3,110170,3 +6291,3,110180,3 +6292,3,110190,3 +6293,3,110200,3 +6294,3,110210,3 +6295,3,110220,3 +6296,3,110230,3 +6297,3,110240,3 +6298,3,110250,3 +6299,3,110260,3 +6300,3,110270,3 +6301,3,110620,3 +6302,3,110670,3 +6303,4,140000,3 +6304,4,150010,3 +6305,4,150020,3 +6306,4,150030,3 +6307,4,150040,3 +6309,3,102000060,1 +6310,3,102000070,1 +6311,3,102000080,1 +6312,3,103000050,1 +6313,3,105000050,1 +6314,3,107000060,1 +6315,3,107000070,1 +6316,3,107000080,1 +6317,3,108000050,1 +6318,3,109000050,1 +6319,3,103000060,1 +6320,3,103000070,1 +6321,3,103000080,1 +6322,3,110000050,1 +6323,4,102000100,1 +6324,4,102000110,1 +6325,4,106000090,1 +6326,4,109000090,1 +6327,4,107000100,1 +6328,4,103000090,1 +6329,4,102000090,1 +6330,4,103000100,1 +6331,5,102000120,1 +6332,5,107000110,1 +6333,5,103000120,1 +6334,1,102000000,2 +6335,2,102000001,2 +6336,3,102000002,2 +6337,4,102000003,2 +6338,5,102000004,2 +6339,1,105000000,2 +6340,2,105000001,2 +6341,3,105000002,2 +6342,4,105000003,2 +6343,5,105000004,2 +6344,1,112000000,2 +6345,2,112000001,2 +6346,3,112000002,2 +6347,4,112000003,2 +6348,5,112000004,2 +6349,4,120024,3 +6350,4,120034,3 +6351,4,120044,3 +6352,4,120054,3 +6353,3,120241,3 +6354,3,120251,3 +6355,3,120261,3 +6356,3,120271,3 +6357,3,120300,3 +6358,3,120310,3 +6359,3,120320,3 +6360,3,120330,3 +6361,3,120340,3 +6362,3,120350,3 +6363,3,120360,3 +6364,3,120370,3 +6365,3,120380,3 +6366,3,120390,3 +6367,3,120400,3 +6368,3,120410,3 +6369,3,120420,3 +6370,3,120430,3 +6371,3,120450,3 +6372,3,120460,3 +6373,3,120550,3 +6374,3,120560,3 +6375,3,120570,3 +6376,3,120990,3 +6377,3,121000,3 +6378,3,121010,3 +6379,3,121020,3 +6380,4,140000,3 +6381,4,150010,3 +6382,4,150020,3 +6383,4,150030,3 +6384,4,150040,3 +6386,3,106000060,1 +6387,3,106000070,1 +6388,3,106000080,1 +6389,3,101000070,1 +6390,3,110000060,1 +6391,3,104000060,1 +6392,3,104000070,1 +6393,3,104000080,1 +6394,3,102000050,1 +6395,3,104000170,1 +6396,3,104000260,1 +6397,4,106000100,1 +6398,4,106000110,1 +6399,4,104000090,1 +6400,4,104000100,1 +6401,4,104000110,1 +6402,4,107000090,1 +6403,4,104000180,1 +6404,5,106000120,1 +6405,5,104000120,1 +6406,5,104000190,1 +6407,1,103000000,2 +6408,2,103000001,2 +6409,3,103000002,2 +6410,4,103000003,2 +6411,5,103000004,2 +6412,1,111000000,2 +6413,2,111000001,2 +6414,3,111000002,2 +6415,4,111000003,2 +6416,5,111000004,2 +6417,1,115000000,2 +6418,2,115000001,2 +6419,3,115000002,2 +6420,4,115000003,2 +6421,5,115000004,2 +6422,4,130024,3 +6423,4,130034,3 +6424,4,130044,3 +6425,4,130054,3 +6426,3,130060,3 +6427,3,130070,3 +6428,3,130080,3 +6429,3,130090,3 +6430,3,130100,3 +6431,3,130110,3 +6432,3,130120,3 +6433,3,130130,3 +6434,3,130140,3 +6435,3,130150,3 +6436,3,130160,3 +6437,3,130170,3 +6438,3,130180,3 +6439,3,130190,3 +6440,3,130200,3 +6441,3,130420,3 +6442,3,130510,3 +6443,3,130520,3 +6444,3,130530,3 +6445,3,130540,3 +6446,3,130660,3 +6447,3,130790,3 +6448,3,130800,3 +6449,4,140000,3 +6450,4,150010,3 +6451,4,150020,3 +6452,4,150030,3 +6453,4,150040,3 +6455,1,101000010,1 +6456,1,102000010,1 +6457,1,103000010,1 +6458,1,104000010,1 +6459,1,105000010,1 +6460,1,106000010,1 +6461,1,107000010,1 +6462,1,108000010,1 +6463,1,109000010,1 +6464,1,110000010,1 +6465,2,101000020,1 +6466,2,101000030,1 +6467,2,102000020,1 +6468,2,102000030,1 +6469,2,102000040,1 +6470,2,103000020,1 +6471,2,103000030,1 +6472,2,103000040,1 +6473,2,104000020,1 +6474,2,104000030,1 +6475,2,104000040,1 +6476,2,105000020,1 +6477,2,105000030,1 +6478,2,105000040,1 +6479,2,106000020,1 +6480,2,106000030,1 +6481,2,106000040,1 +6482,2,107000020,1 +6483,2,107000030,1 +6484,2,107000040,1 +6485,2,108000020,1 +6486,2,108000030,1 +6487,2,108000040,1 +6488,2,109000020,1 +6489,2,109000030,1 +6490,2,109000040,1 +6491,2,110000020,1 +6492,2,110000030,1 +6493,2,110000040,1 +6494,2,118000010,1 +6495,3,101000050,1 +6496,3,101000060,1 +6497,3,101000080,1 +6498,3,101000040,1 +6499,3,109000060,1 +6500,3,109000070,1 +6501,3,109000080,1 +6502,3,105000060,1 +6503,3,105000070,1 +6504,3,105000080,1 +6505,3,104000050,1 +6506,3,106000050,1 +6507,3,102000060,1 +6508,3,102000070,1 +6509,3,102000080,1 +6510,3,103000050,1 +6511,3,105000050,1 +6512,3,107000060,1 +6513,3,107000070,1 +6514,3,107000080,1 +6515,3,108000050,1 +6516,3,109000050,1 +6517,3,103000060,1 +6518,3,103000070,1 +6519,3,103000080,1 +6520,3,110000050,1 +6521,3,106000060,1 +6522,3,106000070,1 +6523,3,106000080,1 +6524,3,101000070,1 +6525,3,110000060,1 +6526,3,104000060,1 +6527,3,104000070,1 +6528,3,104000080,1 +6529,3,102000050,1 +6530,3,104000170,1 +6531,3,104000260,1 +6532,3,111000010,1 +6533,3,111000020,1 +6534,3,111000030,1 +6535,3,112000020,1 +6536,3,112000030,1 +6537,3,108000060,1 +6538,3,108000070,1 +6539,3,108000080,1 +6540,3,107000050,1 +6541,3,112000010,1 +6542,3,110000070,1 +6543,3,110000080,1 +6544,3,118000020,1 +6545,3,118000030,1 +6546,3,118000040,1 +6547,4,101000090,1 +6548,4,101000100,1 +6549,4,101000110,1 +6550,4,109000100,1 +6551,4,105000100,1 +6552,4,105000110,1 +6553,4,108000090,1 +6554,4,110000090,1 +6555,4,102000100,1 +6556,4,102000110,1 +6557,4,106000090,1 +6558,4,109000090,1 +6559,4,107000100,1 +6560,4,103000090,1 +6561,4,102000090,1 +6562,4,103000100,1 +6563,4,106000100,1 +6564,4,106000110,1 +6565,4,104000090,1 +6566,4,104000100,1 +6567,4,104000110,1 +6568,4,107000090,1 +6569,4,104000180,1 +6570,4,111000040,1 +6571,4,112000040,1 +6572,4,108000100,1 +6573,4,105000090,1 +6574,4,110000100,1 +6575,4,118000050,1 +6576,4,118000060,1 +6577,5,101000120,1 +6578,5,109000110,1 +6579,5,105000120,1 +6580,5,102000120,1 +6581,5,107000110,1 +6582,5,103000120,1 +6583,5,106000120,1 +6584,5,104000120,1 +6585,5,104000190,1 +6586,5,111000060,1 +6587,5,112000060,1 +6588,5,108000110,1 +6589,5,110000110,1 +6590,5,118000070,1 +6591,1,170002,3 +6592,2,170003,3 +6593,3,170004,3 +6594,1,180002,3 +6595,2,180003,3 +6596,3,180004,3 +6597,1,201000010,8 +6598,1,292000010,8 +6599,1,299000040,8 +6600,1,299000070,8 +6601,1,299000110,8 +6602,1,299000120,8 +6603,1,299000140,8 +6604,2,202000010,8 +6605,2,290000010,8 +6606,2,299000010,8 +6607,2,299000150,8 +6608,2,299000190,8 +6609,2,299000200,8 +6610,2,299000210,8 +6611,3,298000050,8 +6612,3,298000060,8 +6613,3,299000060,8 +6614,3,299000170,8 +6615,5,297000100,8 +6616,5,291000020,8 +6617,4,140000,3 +6618,5,150010,3 +6619,5,150020,3 +6620,5,150030,3 +6621,5,150040,3 +6623,1,101000010,1 +6624,1,102000010,1 +6625,1,103000010,1 +6626,1,104000010,1 +6627,1,105000010,1 +6628,1,106000010,1 +6629,1,107000010,1 +6630,1,108000010,1 +6631,1,109000010,1 +6632,1,110000010,1 +6633,2,101000020,1 +6634,2,101000030,1 +6635,2,102000020,1 +6636,2,102000030,1 +6637,2,102000040,1 +6638,2,103000020,1 +6639,2,103000030,1 +6640,2,103000040,1 +6641,2,104000020,1 +6642,2,104000030,1 +6643,2,104000040,1 +6644,2,105000020,1 +6645,2,105000030,1 +6646,2,105000040,1 +6647,2,106000020,1 +6648,2,106000030,1 +6649,2,106000040,1 +6650,2,107000020,1 +6651,2,107000030,1 +6652,2,107000040,1 +6653,2,108000020,1 +6654,2,108000030,1 +6655,2,108000040,1 +6656,2,109000020,1 +6657,2,109000030,1 +6658,2,109000040,1 +6659,2,110000020,1 +6660,2,110000030,1 +6661,2,110000040,1 +6662,2,118000010,1 +6663,3,101000050,1 +6664,3,101000060,1 +6665,3,101000080,1 +6666,3,101000040,1 +6667,3,109000060,1 +6668,3,109000070,1 +6669,3,109000080,1 +6670,3,105000060,1 +6671,3,105000070,1 +6672,3,105000080,1 +6673,3,104000050,1 +6674,3,106000050,1 +6675,3,102000060,1 +6676,3,102000070,1 +6677,3,102000080,1 +6678,3,103000050,1 +6679,3,105000050,1 +6680,3,107000060,1 +6681,3,107000070,1 +6682,3,107000080,1 +6683,3,108000050,1 +6684,3,109000050,1 +6685,3,103000060,1 +6686,3,103000070,1 +6687,3,103000080,1 +6688,3,110000050,1 +6689,3,106000060,1 +6690,3,106000070,1 +6691,3,106000080,1 +6692,3,101000070,1 +6693,3,110000060,1 +6694,3,104000060,1 +6695,3,104000070,1 +6696,3,104000080,1 +6697,3,102000050,1 +6698,3,104000170,1 +6699,3,104000260,1 +6700,3,111000010,1 +6701,3,111000020,1 +6702,3,111000030,1 +6703,3,112000020,1 +6704,3,112000030,1 +6705,3,108000060,1 +6706,3,108000070,1 +6707,3,108000080,1 +6708,3,107000050,1 +6709,3,112000010,1 +6710,3,110000070,1 +6711,3,110000080,1 +6712,3,118000020,1 +6713,3,118000030,1 +6714,3,118000040,1 +6715,4,101000090,1 +6716,4,101000100,1 +6717,4,101000110,1 +6718,4,109000100,1 +6719,4,105000100,1 +6720,4,105000110,1 +6721,4,108000090,1 +6722,4,110000090,1 +6723,4,102000100,1 +6724,4,102000110,1 +6725,4,106000090,1 +6726,4,109000090,1 +6727,4,107000100,1 +6728,4,103000090,1 +6729,4,102000090,1 +6730,4,103000100,1 +6731,4,106000100,1 +6732,4,106000110,1 +6733,4,104000090,1 +6734,4,104000100,1 +6735,4,104000110,1 +6736,4,107000090,1 +6737,4,104000180,1 +6738,4,111000040,1 +6739,4,112000040,1 +6740,4,108000100,1 +6741,4,105000090,1 +6742,4,110000100,1 +6743,4,118000050,1 +6744,4,118000060,1 +6745,5,101000120,1 +6746,5,109000110,1 +6747,5,105000120,1 +6748,5,102000120,1 +6749,5,107000110,1 +6750,5,103000120,1 +6751,5,106000120,1 +6752,5,104000120,1 +6753,5,104000190,1 +6754,5,111000060,1 +6755,5,112000060,1 +6756,5,108000110,1 +6757,5,110000110,1 +6758,5,118000070,1 +6759,2,170003,3 +6760,3,170004,3 +6761,2,180003,3 +6762,3,180004,3 +6763,1,201000010,8 +6764,1,292000010,8 +6765,1,299000040,8 +6766,1,299000070,8 +6767,1,299000110,8 +6768,1,299000120,8 +6769,1,299000140,8 +6770,2,202000010,8 +6771,2,290000010,8 +6772,2,299000010,8 +6773,2,299000150,8 +6774,2,299000190,8 +6775,2,299000200,8 +6776,2,299000210,8 +6777,3,298000050,8 +6778,3,298000060,8 +6779,3,299000060,8 +6780,3,299000170,8 +6781,5,297000100,8 +6782,5,291000020,8 +6783,4,140000,3 +6784,5,150010,3 +6785,5,150020,3 +6786,5,150030,3 +6787,5,150040,3 +6789,3,101000050,1 +6790,3,101000060,1 +6791,3,101000080,1 +6792,3,101000040,1 +6793,3,109000060,1 +6794,3,109000070,1 +6795,3,109000080,1 +6796,3,105000060,1 +6797,3,105000070,1 +6798,3,105000080,1 +6799,3,104000050,1 +6800,3,106000050,1 +6801,3,102000060,1 +6802,3,102000070,1 +6803,3,102000080,1 +6804,3,103000050,1 +6805,3,105000050,1 +6806,3,107000060,1 +6807,3,107000070,1 +6808,3,107000080,1 +6809,3,108000050,1 +6810,3,109000050,1 +6811,3,103000060,1 +6812,3,103000070,1 +6813,3,103000080,1 +6814,3,110000050,1 +6815,3,106000060,1 +6816,3,106000070,1 +6817,3,106000080,1 +6818,3,101000070,1 +6819,3,110000060,1 +6820,3,104000060,1 +6821,3,104000070,1 +6822,3,104000080,1 +6823,3,102000050,1 +6824,3,104000170,1 +6825,3,104000260,1 +6826,3,111000010,1 +6827,3,111000020,1 +6828,3,111000030,1 +6829,3,112000020,1 +6830,3,112000030,1 +6831,3,108000060,1 +6832,3,108000070,1 +6833,3,108000080,1 +6834,3,107000050,1 +6835,3,112000010,1 +6836,3,110000070,1 +6837,3,110000080,1 +6838,3,118000020,1 +6839,3,118000030,1 +6840,3,118000040,1 +6841,4,101000090,1 +6842,4,101000100,1 +6843,4,101000110,1 +6844,4,109000100,1 +6845,4,105000100,1 +6846,4,105000110,1 +6847,4,108000090,1 +6848,4,110000090,1 +6849,4,102000100,1 +6850,4,102000110,1 +6851,4,106000090,1 +6852,4,109000090,1 +6853,4,107000100,1 +6854,4,103000090,1 +6855,4,102000090,1 +6856,4,103000100,1 +6857,4,106000100,1 +6858,4,106000110,1 +6859,4,104000090,1 +6860,4,104000100,1 +6861,4,104000110,1 +6862,4,107000090,1 +6863,4,104000180,1 +6864,4,111000040,1 +6865,4,112000040,1 +6866,4,108000100,1 +6867,4,105000090,1 +6868,4,110000100,1 +6869,4,118000050,1 +6870,4,118000060,1 +6871,5,101000120,1 +6872,5,109000110,1 +6873,5,105000120,1 +6874,5,102000120,1 +6875,5,107000110,1 +6876,5,103000120,1 +6877,5,106000120,1 +6878,5,104000120,1 +6879,5,104000190,1 +6880,5,111000060,1 +6881,5,112000060,1 +6882,5,108000110,1 +6883,5,110000110,1 +6884,5,118000070,1 +6885,3,170004,3 +6886,3,180004,3 +6887,1,201000010,8 +6888,1,292000010,8 +6889,1,299000040,8 +6890,1,299000070,8 +6891,1,299000110,8 +6892,1,299000120,8 +6893,1,299000140,8 +6894,2,202000010,8 +6895,2,290000010,8 +6896,2,299000010,8 +6897,2,299000150,8 +6898,2,299000190,8 +6899,2,299000200,8 +6900,2,299000210,8 +6901,3,298000050,8 +6902,3,298000060,8 +6903,3,299000060,8 +6904,3,299000170,8 +6905,5,297000100,8 +6906,5,291000020,8 +6907,4,140000,3 +6908,5,150010,3 +6909,5,150020,3 +6910,5,150030,3 +6911,5,150040,3 +6913,3,101000050,1 +6914,3,101000060,1 +6915,3,101000080,1 +6916,3,101000040,1 +6917,3,109000060,1 +6918,3,109000070,1 +6919,3,109000080,1 +6920,3,105000060,1 +6921,3,105000070,1 +6922,3,105000080,1 +6923,3,104000050,1 +6924,3,106000050,1 +6925,3,102000060,1 +6926,3,102000070,1 +6927,3,102000080,1 +6928,3,103000050,1 +6929,3,105000050,1 +6930,3,107000060,1 +6931,3,107000070,1 +6932,3,107000080,1 +6933,3,108000050,1 +6934,3,109000050,1 +6935,3,103000060,1 +6936,3,103000070,1 +6937,3,103000080,1 +6938,3,110000050,1 +6939,3,106000060,1 +6940,3,106000070,1 +6941,3,106000080,1 +6942,3,101000070,1 +6943,3,110000060,1 +6944,3,104000060,1 +6945,3,104000070,1 +6946,3,104000080,1 +6947,3,102000050,1 +6948,3,104000170,1 +6949,3,104000260,1 +6950,3,111000010,1 +6951,3,111000020,1 +6952,3,111000030,1 +6953,3,112000020,1 +6954,3,112000030,1 +6955,3,108000060,1 +6956,3,108000070,1 +6957,3,108000080,1 +6958,3,107000050,1 +6959,3,112000010,1 +6960,3,110000070,1 +6961,3,110000080,1 +6962,3,118000020,1 +6963,3,118000030,1 +6964,3,118000040,1 +6965,4,101000090,1 +6966,4,101000100,1 +6967,4,101000110,1 +6968,4,109000100,1 +6969,4,105000100,1 +6970,4,105000110,1 +6971,4,108000090,1 +6972,4,110000090,1 +6973,4,102000100,1 +6974,4,102000110,1 +6975,4,106000090,1 +6976,4,109000090,1 +6977,4,107000100,1 +6978,4,103000090,1 +6979,4,102000090,1 +6980,4,103000100,1 +6981,4,106000100,1 +6982,4,106000110,1 +6983,4,104000090,1 +6984,4,104000100,1 +6985,4,104000110,1 +6986,4,107000090,1 +6987,4,104000180,1 +6988,4,111000040,1 +6989,4,112000040,1 +6990,4,108000100,1 +6991,4,105000090,1 +6992,4,110000100,1 +6993,4,118000050,1 +6994,4,118000060,1 +6995,5,101000120,1 +6996,5,109000110,1 +6997,5,105000120,1 +6998,5,102000120,1 +6999,5,107000110,1 +7000,5,103000120,1 +7001,5,106000120,1 +7002,5,104000120,1 +7003,5,104000190,1 +7004,5,111000060,1 +7005,5,112000060,1 +7006,5,108000110,1 +7007,5,110000110,1 +7008,5,118000070,1 +7009,3,170004,3 +7010,3,180004,3 +7011,1,201000010,8 +7012,1,292000010,8 +7013,1,299000040,8 +7014,1,299000070,8 +7015,1,299000110,8 +7016,1,299000120,8 +7017,1,299000140,8 +7018,2,202000010,8 +7019,2,290000010,8 +7020,2,299000010,8 +7021,2,299000150,8 +7022,2,299000190,8 +7023,2,299000200,8 +7024,2,299000210,8 +7025,3,298000050,8 +7026,3,298000060,8 +7027,3,299000060,8 +7028,3,299000170,8 +7029,5,297000100,8 +7030,5,291000020,8 +7031,4,140000,3 +7032,5,150010,3 +7033,5,150020,3 +7034,5,150030,3 +7035,5,150040,3 +7037,3,101000050,1 +7038,3,101000060,1 +7039,3,101000080,1 +7040,3,101000040,1 +7041,3,109000060,1 +7042,3,109000070,1 +7043,3,109000080,1 +7044,3,105000060,1 +7045,3,105000070,1 +7046,3,105000080,1 +7047,3,104000050,1 +7048,3,106000050,1 +7049,3,102000060,1 +7050,3,102000070,1 +7051,3,102000080,1 +7052,3,103000050,1 +7053,3,105000050,1 +7054,3,107000060,1 +7055,3,107000070,1 +7056,3,107000080,1 +7057,3,108000050,1 +7058,3,109000050,1 +7059,3,103000060,1 +7060,3,103000070,1 +7061,3,103000080,1 +7062,3,110000050,1 +7063,3,106000060,1 +7064,3,106000070,1 +7065,3,106000080,1 +7066,3,101000070,1 +7067,3,110000060,1 +7068,3,104000060,1 +7069,3,104000070,1 +7070,3,104000080,1 +7071,3,102000050,1 +7072,3,104000170,1 +7073,3,104000260,1 +7074,3,111000010,1 +7075,3,111000020,1 +7076,3,111000030,1 +7077,3,112000020,1 +7078,3,112000030,1 +7079,3,108000060,1 +7080,3,108000070,1 +7081,3,108000080,1 +7082,3,107000050,1 +7083,3,112000010,1 +7084,3,110000070,1 +7085,3,110000080,1 +7086,3,118000020,1 +7087,3,118000030,1 +7088,3,118000040,1 +7089,4,101000090,1 +7090,4,101000100,1 +7091,4,101000110,1 +7092,4,109000100,1 +7093,4,105000100,1 +7094,4,105000110,1 +7095,4,108000090,1 +7096,4,110000090,1 +7097,4,102000100,1 +7098,4,102000110,1 +7099,4,106000090,1 +7100,4,109000090,1 +7101,4,107000100,1 +7102,4,103000090,1 +7103,4,102000090,1 +7104,4,103000100,1 +7105,4,106000100,1 +7106,4,106000110,1 +7107,4,104000090,1 +7108,4,104000100,1 +7109,4,104000110,1 +7110,4,107000090,1 +7111,4,104000180,1 +7112,4,111000040,1 +7113,4,112000040,1 +7114,4,108000100,1 +7115,4,105000090,1 +7116,4,110000100,1 +7117,4,118000050,1 +7118,4,118000060,1 +7119,5,101000120,1 +7120,5,109000110,1 +7121,5,105000120,1 +7122,5,102000120,1 +7123,5,107000110,1 +7124,5,103000120,1 +7125,5,106000120,1 +7126,5,104000120,1 +7127,5,104000190,1 +7128,5,111000060,1 +7129,5,112000060,1 +7130,5,108000110,1 +7131,5,110000110,1 +7132,5,118000070,1 +7133,3,170004,3 +7134,3,180004,3 +7135,1,201000010,8 +7136,1,292000010,8 +7137,1,299000040,8 +7138,1,299000070,8 +7139,1,299000110,8 +7140,1,299000120,8 +7141,1,299000140,8 +7142,2,202000010,8 +7143,2,290000010,8 +7144,2,299000010,8 +7145,2,299000150,8 +7146,2,299000190,8 +7147,2,299000200,8 +7148,2,299000210,8 +7149,3,298000050,8 +7150,3,298000060,8 +7151,3,299000060,8 +7152,3,299000170,8 +7153,5,297000100,8 +7154,5,291000020,8 +7155,4,140000,3 +7156,5,150010,3 +7157,5,150020,3 +7158,5,150030,3 +7159,5,150040,3 +7160,5,190000,3 +7161,5,200000,3 +7162,5,210000,3 +7164,3,118000020,1 +7165,3,118000030,1 +7166,3,118000040,1 +7167,3,106000060,1 +7168,3,106000070,1 +7169,3,106000080,1 +7170,3,101000070,1 +7171,3,110000060,1 +7172,3,104000060,1 +7173,3,104000070,1 +7174,3,104000080,1 +7175,3,102000050,1 +7176,3,104000170,1 +7177,3,104000260,1 +7178,4,118000050,1 +7179,4,118000060,1 +7180,4,106000100,1 +7181,4,106000110,1 +7182,4,104000090,1 +7183,4,104000100,1 +7184,4,104000110,1 +7185,4,104000270,1 +7186,4,107000090,1 +7187,4,104000180,1 +7188,5,118000070,1 +7189,5,106000120,1 +7190,5,104000120,1 +7191,5,104000190,1 +7192,1,103000000,2 +7193,2,103000001,2 +7194,3,103000002,2 +7195,4,103000003,2 +7196,5,103000004,2 +7197,1,111000000,2 +7198,2,111000001,2 +7199,3,111000002,2 +7200,4,111000003,2 +7201,5,111000004,2 +7202,1,115000000,2 +7203,2,115000001,2 +7204,3,115000002,2 +7205,4,115000003,2 +7206,5,115000004,2 +7207,3,170004,3 +7208,4,170005,3 +7209,3,180004,3 +7210,4,180005,3 +7211,4,140000,3 +7212,4,150010,3 +7213,4,150020,3 +7214,4,150030,3 +7215,4,150040,3 +7217,3,111000010,1 +7218,3,111000020,1 +7219,3,111000030,1 +7220,3,112000020,1 +7221,3,112000030,1 +7222,3,108000060,1 +7223,3,108000070,1 +7224,3,108000080,1 +7225,3,107000050,1 +7226,3,112000010,1 +7227,3,110000070,1 +7228,3,110000080,1 +7229,4,111000040,1 +7230,4,112000040,1 +7231,4,108000100,1 +7232,4,105000090,1 +7233,4,110000100,1 +7234,5,111000060,1 +7235,5,112000060,1 +7236,5,108000110,1 +7237,5,110000110,1 +7238,1,108000000,2 +7239,2,108000001,2 +7240,3,108000002,2 +7241,4,108000003,2 +7242,5,108000004,2 +7243,1,107000000,2 +7244,2,107000001,2 +7245,3,107000002,2 +7246,4,107000003,2 +7247,5,107000004,2 +7248,1,120000000,2 +7249,2,120000001,2 +7250,3,120000002,2 +7251,4,120000003,2 +7252,5,120000004,2 +7253,4,110024,3 +7254,4,110034,3 +7255,4,110044,3 +7256,4,110054,3 +7257,3,110060,3 +7258,3,110070,3 +7259,3,110080,3 +7260,3,110090,3 +7261,3,110100,3 +7262,3,110110,3 +7263,3,110120,3 +7264,3,110130,3 +7265,3,110140,3 +7266,3,110150,3 +7267,3,110160,3 +7268,3,110170,3 +7269,3,110180,3 +7270,3,110190,3 +7271,3,110200,3 +7272,3,110210,3 +7273,3,110220,3 +7274,3,110230,3 +7275,3,110240,3 +7276,3,110250,3 +7277,3,110260,3 +7278,3,110270,3 +7279,3,110620,3 +7280,3,110670,3 +7281,4,140000,3 +7282,4,150010,3 +7283,4,150020,3 +7284,4,150030,3 +7285,4,150040,3 +7287,3,101000050,1 +7288,3,101000060,1 +7289,3,101000080,1 +7290,3,101000040,1 +7291,3,109000060,1 +7292,3,109000070,1 +7293,3,109000080,1 +7294,3,105000060,1 +7295,3,105000070,1 +7296,3,105000080,1 +7297,3,104000050,1 +7298,3,106000050,1 +7299,4,101000090,1 +7300,4,101000100,1 +7301,4,101000110,1 +7302,4,109000100,1 +7303,4,105000100,1 +7304,4,105000110,1 +7305,4,108000090,1 +7306,4,110000090,1 +7307,5,101000120,1 +7308,5,109000110,1 +7309,5,105000120,1 +7310,1,101000000,2 +7311,2,101000001,2 +7312,3,101000002,2 +7313,4,101000008,2 +7314,5,101000004,2 +7315,1,109000000,2 +7316,2,109000001,2 +7317,3,109000002,2 +7318,4,109000003,2 +7319,5,109000004,2 +7320,4,120024,3 +7321,4,120034,3 +7322,4,120044,3 +7323,4,120054,3 +7324,3,120241,3 +7325,3,120251,3 +7326,3,120261,3 +7327,3,120271,3 +7328,3,120300,3 +7329,3,120310,3 +7330,3,120320,3 +7331,3,120330,3 +7332,3,120340,3 +7333,3,120350,3 +7334,3,120360,3 +7335,3,120370,3 +7336,3,120380,3 +7337,3,120390,3 +7338,3,120400,3 +7339,3,120410,3 +7340,3,120420,3 +7341,3,120430,3 +7342,3,120450,3 +7343,3,120460,3 +7344,3,120550,3 +7345,3,120560,3 +7346,3,120570,3 +7347,3,120990,3 +7348,3,121000,3 +7349,3,121010,3 +7350,3,121020,3 +7351,4,140000,3 +7352,4,150010,3 +7353,4,150020,3 +7354,4,150030,3 +7355,4,150040,3 +7357,3,102000060,1 +7358,3,102000070,1 +7359,3,102000080,1 +7360,3,103000050,1 +7361,3,105000050,1 +7362,3,107000060,1 +7363,3,107000070,1 +7364,3,107000080,1 +7365,3,108000050,1 +7366,3,109000050,1 +7367,3,103000060,1 +7368,3,103000070,1 +7369,3,103000080,1 +7370,3,110000050,1 +7371,4,102000100,1 +7372,4,102000110,1 +7373,4,106000090,1 +7374,4,109000090,1 +7375,4,107000100,1 +7376,4,103000090,1 +7377,4,102000090,1 +7378,4,103000100,1 +7379,5,102000120,1 +7380,5,107000110,1 +7381,5,103000120,1 +7382,1,102000000,2 +7383,2,102000001,2 +7384,3,102000002,2 +7385,4,102000003,2 +7386,5,102000004,2 +7387,1,105000000,2 +7388,2,105000001,2 +7389,3,105000002,2 +7390,4,105000003,2 +7391,5,105000004,2 +7392,1,112000000,2 +7393,2,112000001,2 +7394,3,112000002,2 +7395,4,112000003,2 +7396,5,112000004,2 +7397,4,130024,3 +7398,4,130034,3 +7399,4,130044,3 +7400,4,130054,3 +7401,3,130060,3 +7402,3,130070,3 +7403,3,130080,3 +7404,3,130090,3 +7405,3,130100,3 +7406,3,130110,3 +7407,3,130120,3 +7408,3,130130,3 +7409,3,130140,3 +7410,3,130150,3 +7411,3,130160,3 +7412,3,130170,3 +7413,3,130180,3 +7414,3,130190,3 +7415,3,130200,3 +7416,3,130420,3 +7417,3,130510,3 +7418,3,130520,3 +7419,3,130530,3 +7420,3,130540,3 +7421,3,130660,3 +7422,3,130790,3 +7423,3,130800,3 +7424,4,140000,3 +7425,4,150010,3 +7426,4,150020,3 +7427,4,150030,3 +7428,4,150040,3 +7430,1,101000010,1 +7431,1,102000010,1 +7432,1,103000010,1 +7433,1,104000010,1 +7434,1,105000010,1 +7435,1,106000010,1 +7436,1,107000010,1 +7437,1,108000010,1 +7438,1,109000010,1 +7439,1,110000010,1 +7440,2,101000020,1 +7441,2,101000030,1 +7442,2,102000020,1 +7443,2,102000030,1 +7444,2,102000040,1 +7445,2,103000020,1 +7446,2,103000030,1 +7447,2,103000040,1 +7448,2,104000020,1 +7449,2,104000030,1 +7450,2,104000040,1 +7451,2,105000020,1 +7452,2,105000030,1 +7453,2,105000040,1 +7454,2,106000020,1 +7455,2,106000030,1 +7456,2,106000040,1 +7457,2,107000020,1 +7458,2,107000030,1 +7459,2,107000040,1 +7460,2,108000020,1 +7461,2,108000030,1 +7462,2,108000040,1 +7463,2,109000020,1 +7464,2,109000030,1 +7465,2,109000040,1 +7466,2,110000020,1 +7467,2,110000030,1 +7468,2,110000040,1 +7469,2,118000010,1 +7470,3,101000050,1 +7471,3,101000060,1 +7472,3,101000080,1 +7473,3,101000040,1 +7474,3,109000060,1 +7475,3,109000070,1 +7476,3,109000080,1 +7477,3,105000060,1 +7478,3,105000070,1 +7479,3,105000080,1 +7480,3,104000050,1 +7481,3,106000050,1 +7482,3,102000060,1 +7483,3,102000070,1 +7484,3,102000080,1 +7485,3,103000050,1 +7486,3,105000050,1 +7487,3,107000060,1 +7488,3,107000070,1 +7489,3,107000080,1 +7490,3,108000050,1 +7491,3,109000050,1 +7492,3,103000060,1 +7493,3,103000070,1 +7494,3,103000080,1 +7495,3,110000050,1 +7496,3,106000060,1 +7497,3,106000070,1 +7498,3,106000080,1 +7499,3,101000070,1 +7500,3,110000060,1 +7501,3,104000060,1 +7502,3,104000070,1 +7503,3,104000080,1 +7504,3,102000050,1 +7505,3,104000170,1 +7506,3,104000260,1 +7507,3,111000010,1 +7508,3,111000020,1 +7509,3,111000030,1 +7510,3,112000020,1 +7511,3,112000030,1 +7512,3,108000060,1 +7513,3,108000070,1 +7514,3,108000080,1 +7515,3,107000050,1 +7516,3,112000010,1 +7517,3,110000070,1 +7518,3,110000080,1 +7519,3,118000020,1 +7520,3,118000030,1 +7521,3,118000040,1 +7522,4,101000090,1 +7523,4,101000100,1 +7524,4,101000110,1 +7525,4,109000100,1 +7526,4,105000100,1 +7527,4,105000110,1 +7528,4,108000090,1 +7529,4,110000090,1 +7530,4,102000100,1 +7531,4,102000110,1 +7532,4,106000090,1 +7533,4,109000090,1 +7534,4,107000100,1 +7535,4,103000090,1 +7536,4,102000090,1 +7537,4,103000100,1 +7538,4,106000100,1 +7539,4,106000110,1 +7540,4,104000090,1 +7541,4,104000100,1 +7542,4,104000110,1 +7543,4,107000090,1 +7544,4,104000180,1 +7545,4,111000040,1 +7546,4,112000040,1 +7547,4,108000100,1 +7548,4,105000090,1 +7549,4,110000100,1 +7550,4,118000050,1 +7551,4,118000060,1 +7552,5,101000120,1 +7553,5,109000110,1 +7554,5,105000120,1 +7555,5,102000120,1 +7556,5,107000110,1 +7557,5,103000120,1 +7558,5,106000120,1 +7559,5,104000120,1 +7560,5,104000190,1 +7561,5,111000060,1 +7562,5,112000060,1 +7563,5,108000110,1 +7564,5,110000110,1 +7565,5,118000070,1 +7566,1,170002,3 +7567,2,170003,3 +7568,3,170004,3 +7569,1,180002,3 +7570,2,180003,3 +7571,3,180004,3 +7572,1,201000010,8 +7573,1,292000010,8 +7574,1,299000040,8 +7575,1,299000070,8 +7576,1,299000110,8 +7577,1,299000120,8 +7578,1,299000140,8 +7579,2,202000010,8 +7580,2,290000010,8 +7581,2,299000010,8 +7582,2,299000150,8 +7583,2,299000190,8 +7584,2,299000200,8 +7585,2,299000210,8 +7586,3,298000050,8 +7587,3,298000060,8 +7588,3,299000060,8 +7589,3,299000170,8 +7590,3,290000120,8 +7591,3,291000050,8 +7592,3,292000020,8 +7593,4,299000670,8 +7594,4,299000680,8 +7595,4,204000010,8 +7596,5,297000100,8 +7597,5,291000020,8 +7598,5,297000130,8 +7599,5,297000140,8 +7600,5,203000010,8 +7601,4,140000,3 +7602,5,150010,3 +7603,5,150020,3 +7604,5,150030,3 +7605,5,150040,3 +7607,1,101000010,1 +7608,1,102000010,1 +7609,1,103000010,1 +7610,1,104000010,1 +7611,1,105000010,1 +7612,1,106000010,1 +7613,1,107000010,1 +7614,1,108000010,1 +7615,1,109000010,1 +7616,1,110000010,1 +7617,2,101000020,1 +7618,2,101000030,1 +7619,2,102000020,1 +7620,2,102000030,1 +7621,2,102000040,1 +7622,2,103000020,1 +7623,2,103000030,1 +7624,2,103000040,1 +7625,2,104000020,1 +7626,2,104000030,1 +7627,2,104000040,1 +7628,2,105000020,1 +7629,2,105000030,1 +7630,2,105000040,1 +7631,2,106000020,1 +7632,2,106000030,1 +7633,2,106000040,1 +7634,2,107000020,1 +7635,2,107000030,1 +7636,2,107000040,1 +7637,2,108000020,1 +7638,2,108000030,1 +7639,2,108000040,1 +7640,2,109000020,1 +7641,2,109000030,1 +7642,2,109000040,1 +7643,2,110000020,1 +7644,2,110000030,1 +7645,2,110000040,1 +7646,2,118000010,1 +7647,3,101000050,1 +7648,3,101000060,1 +7649,3,101000080,1 +7650,3,101000040,1 +7651,3,109000060,1 +7652,3,109000070,1 +7653,3,109000080,1 +7654,3,105000060,1 +7655,3,105000070,1 +7656,3,105000080,1 +7657,3,104000050,1 +7658,3,106000050,1 +7659,3,102000060,1 +7660,3,102000070,1 +7661,3,102000080,1 +7662,3,103000050,1 +7663,3,105000050,1 +7664,3,107000060,1 +7665,3,107000070,1 +7666,3,107000080,1 +7667,3,108000050,1 +7668,3,109000050,1 +7669,3,103000060,1 +7670,3,103000070,1 +7671,3,103000080,1 +7672,3,110000050,1 +7673,3,106000060,1 +7674,3,106000070,1 +7675,3,106000080,1 +7676,3,101000070,1 +7677,3,110000060,1 +7678,3,104000060,1 +7679,3,104000070,1 +7680,3,104000080,1 +7681,3,102000050,1 +7682,3,104000170,1 +7683,3,104000260,1 +7684,3,111000010,1 +7685,3,111000020,1 +7686,3,111000030,1 +7687,3,112000020,1 +7688,3,112000030,1 +7689,3,108000060,1 +7690,3,108000070,1 +7691,3,108000080,1 +7692,3,107000050,1 +7693,3,112000010,1 +7694,3,110000070,1 +7695,3,110000080,1 +7696,3,118000020,1 +7697,3,118000030,1 +7698,3,118000040,1 +7699,4,101000090,1 +7700,4,101000100,1 +7701,4,101000110,1 +7702,4,109000100,1 +7703,4,105000100,1 +7704,4,105000110,1 +7705,4,108000090,1 +7706,4,110000090,1 +7707,4,102000100,1 +7708,4,102000110,1 +7709,4,106000090,1 +7710,4,109000090,1 +7711,4,107000100,1 +7712,4,103000090,1 +7713,4,102000090,1 +7714,4,103000100,1 +7715,4,106000100,1 +7716,4,106000110,1 +7717,4,104000090,1 +7718,4,104000100,1 +7719,4,104000110,1 +7720,4,107000090,1 +7721,4,104000180,1 +7722,4,111000040,1 +7723,4,112000040,1 +7724,4,108000100,1 +7725,4,105000090,1 +7726,4,110000100,1 +7727,4,118000050,1 +7728,4,118000060,1 +7729,5,101000120,1 +7730,5,109000110,1 +7731,5,105000120,1 +7732,5,102000120,1 +7733,5,107000110,1 +7734,5,103000120,1 +7735,5,106000120,1 +7736,5,104000120,1 +7737,5,104000190,1 +7738,5,111000060,1 +7739,5,112000060,1 +7740,5,108000110,1 +7741,5,110000110,1 +7742,5,118000070,1 +7743,2,170003,3 +7744,3,170004,3 +7745,2,180003,3 +7746,3,180004,3 +7747,1,201000010,8 +7748,1,292000010,8 +7749,1,299000040,8 +7750,1,299000070,8 +7751,1,299000110,8 +7752,1,299000120,8 +7753,1,299000140,8 +7754,2,202000010,8 +7755,2,290000010,8 +7756,2,299000010,8 +7757,2,299000150,8 +7758,2,299000190,8 +7759,2,299000200,8 +7760,2,299000210,8 +7761,3,298000050,8 +7762,3,298000060,8 +7763,3,299000060,8 +7764,3,299000170,8 +7765,3,290000120,8 +7766,3,291000050,8 +7767,3,292000020,8 +7768,4,299000670,8 +7769,4,299000680,8 +7770,4,204000010,8 +7771,5,297000100,8 +7772,5,291000020,8 +7773,5,297000130,8 +7774,5,297000140,8 +7775,5,203000010,8 +7776,4,140000,3 +7777,5,150010,3 +7778,5,150020,3 +7779,5,150030,3 +7780,5,150040,3 +7782,3,101000050,1 +7783,3,101000060,1 +7784,3,101000080,1 +7785,3,101000040,1 +7786,3,109000060,1 +7787,3,109000070,1 +7788,3,109000080,1 +7789,3,105000060,1 +7790,3,105000070,1 +7791,3,105000080,1 +7792,3,104000050,1 +7793,3,106000050,1 +7794,3,102000060,1 +7795,3,102000070,1 +7796,3,102000080,1 +7797,3,103000050,1 +7798,3,105000050,1 +7799,3,107000060,1 +7800,3,107000070,1 +7801,3,107000080,1 +7802,3,108000050,1 +7803,3,109000050,1 +7804,3,103000060,1 +7805,3,103000070,1 +7806,3,103000080,1 +7807,3,110000050,1 +7808,3,106000060,1 +7809,3,106000070,1 +7810,3,106000080,1 +7811,3,101000070,1 +7812,3,110000060,1 +7813,3,104000060,1 +7814,3,104000070,1 +7815,3,104000080,1 +7816,3,102000050,1 +7817,3,104000170,1 +7818,3,104000260,1 +7819,3,111000010,1 +7820,3,111000020,1 +7821,3,111000030,1 +7822,3,112000020,1 +7823,3,112000030,1 +7824,3,108000060,1 +7825,3,108000070,1 +7826,3,108000080,1 +7827,3,107000050,1 +7828,3,112000010,1 +7829,3,110000070,1 +7830,3,110000080,1 +7831,3,118000020,1 +7832,3,118000030,1 +7833,3,118000040,1 +7834,4,101000090,1 +7835,4,101000100,1 +7836,4,101000110,1 +7837,4,109000100,1 +7838,4,105000100,1 +7839,4,105000110,1 +7840,4,108000090,1 +7841,4,110000090,1 +7842,4,102000100,1 +7843,4,102000110,1 +7844,4,106000090,1 +7845,4,109000090,1 +7846,4,107000100,1 +7847,4,103000090,1 +7848,4,102000090,1 +7849,4,103000100,1 +7850,4,106000100,1 +7851,4,106000110,1 +7852,4,104000090,1 +7853,4,104000100,1 +7854,4,104000110,1 +7855,4,107000090,1 +7856,4,104000180,1 +7857,4,111000040,1 +7858,4,112000040,1 +7859,4,108000100,1 +7860,4,105000090,1 +7861,4,110000100,1 +7862,4,118000050,1 +7863,4,118000060,1 +7864,5,101000120,1 +7865,5,109000110,1 +7866,5,105000120,1 +7867,5,102000120,1 +7868,5,107000110,1 +7869,5,103000120,1 +7870,5,106000120,1 +7871,5,104000120,1 +7872,5,104000190,1 +7873,5,111000060,1 +7874,5,112000060,1 +7875,5,108000110,1 +7876,5,110000110,1 +7877,5,118000070,1 +7878,3,170004,3 +7879,3,180004,3 +7880,1,201000010,8 +7881,1,292000010,8 +7882,1,299000040,8 +7883,1,299000070,8 +7884,1,299000110,8 +7885,1,299000120,8 +7886,1,299000140,8 +7887,2,202000010,8 +7888,2,290000010,8 +7889,2,299000010,8 +7890,2,299000150,8 +7891,2,299000190,8 +7892,2,299000200,8 +7893,2,299000210,8 +7894,3,298000050,8 +7895,3,298000060,8 +7896,3,299000060,8 +7897,3,299000170,8 +7898,3,290000120,8 +7899,3,291000050,8 +7900,3,292000020,8 +7901,4,299000670,8 +7902,4,299000680,8 +7903,4,204000010,8 +7904,5,297000100,8 +7905,5,291000020,8 +7906,5,297000130,8 +7907,5,297000140,8 +7908,5,203000010,8 +7909,4,140000,3 +7910,5,150010,3 +7911,5,150020,3 +7912,5,150030,3 +7913,5,150040,3 +7915,3,101000050,1 +7916,3,101000060,1 +7917,3,101000080,1 +7918,3,101000040,1 +7919,3,109000060,1 +7920,3,109000070,1 +7921,3,109000080,1 +7922,3,105000060,1 +7923,3,105000070,1 +7924,3,105000080,1 +7925,3,104000050,1 +7926,3,106000050,1 +7927,3,102000060,1 +7928,3,102000070,1 +7929,3,102000080,1 +7930,3,103000050,1 +7931,3,105000050,1 +7932,3,107000060,1 +7933,3,107000070,1 +7934,3,107000080,1 +7935,3,108000050,1 +7936,3,109000050,1 +7937,3,103000060,1 +7938,3,103000070,1 +7939,3,103000080,1 +7940,3,110000050,1 +7941,3,106000060,1 +7942,3,106000070,1 +7943,3,106000080,1 +7944,3,101000070,1 +7945,3,110000060,1 +7946,3,104000060,1 +7947,3,104000070,1 +7948,3,104000080,1 +7949,3,102000050,1 +7950,3,104000170,1 +7951,3,104000260,1 +7952,3,111000010,1 +7953,3,111000020,1 +7954,3,111000030,1 +7955,3,112000020,1 +7956,3,112000030,1 +7957,3,108000060,1 +7958,3,108000070,1 +7959,3,108000080,1 +7960,3,107000050,1 +7961,3,112000010,1 +7962,3,110000070,1 +7963,3,110000080,1 +7964,3,118000020,1 +7965,3,118000030,1 +7966,3,118000040,1 +7967,4,101000090,1 +7968,4,101000100,1 +7969,4,101000110,1 +7970,4,109000100,1 +7971,4,105000100,1 +7972,4,105000110,1 +7973,4,108000090,1 +7974,4,110000090,1 +7975,4,102000100,1 +7976,4,102000110,1 +7977,4,106000090,1 +7978,4,109000090,1 +7979,4,107000100,1 +7980,4,103000090,1 +7981,4,102000090,1 +7982,4,103000100,1 +7983,4,106000100,1 +7984,4,106000110,1 +7985,4,104000090,1 +7986,4,104000100,1 +7987,4,104000110,1 +7988,4,107000090,1 +7989,4,104000180,1 +7990,4,111000040,1 +7991,4,112000040,1 +7992,4,108000100,1 +7993,4,105000090,1 +7994,4,110000100,1 +7995,4,118000050,1 +7996,4,118000060,1 +7997,5,101000120,1 +7998,5,109000110,1 +7999,5,105000120,1 +8000,5,102000120,1 +8001,5,107000110,1 +8002,5,103000120,1 +8003,5,106000120,1 +8004,5,104000120,1 +8005,5,104000190,1 +8006,5,111000060,1 +8007,5,112000060,1 +8008,5,108000110,1 +8009,5,110000110,1 +8010,5,118000070,1 +8011,3,170004,3 +8012,3,180004,3 +8013,1,201000010,8 +8014,1,292000010,8 +8015,1,299000040,8 +8016,1,299000070,8 +8017,1,299000110,8 +8018,1,299000120,8 +8019,1,299000140,8 +8020,2,202000010,8 +8021,2,290000010,8 +8022,2,299000010,8 +8023,2,299000150,8 +8024,2,299000190,8 +8025,2,299000200,8 +8026,2,299000210,8 +8027,3,298000050,8 +8028,3,298000060,8 +8029,3,299000060,8 +8030,3,299000170,8 +8031,3,290000120,8 +8032,3,291000050,8 +8033,3,292000020,8 +8034,4,299000670,8 +8035,4,299000680,8 +8036,4,204000010,8 +8037,5,297000100,8 +8038,5,291000020,8 +8039,5,297000130,8 +8040,5,297000140,8 +8041,5,203000010,8 +8042,4,140000,3 +8043,5,150010,3 +8044,5,150020,3 +8045,5,150030,3 +8046,5,150040,3 +8048,3,101000050,1 +8049,3,101000060,1 +8050,3,101000080,1 +8051,3,101000040,1 +8052,3,109000060,1 +8053,3,109000070,1 +8054,3,109000080,1 +8055,3,105000060,1 +8056,3,105000070,1 +8057,3,105000080,1 +8058,3,104000050,1 +8059,3,106000050,1 +8060,3,102000060,1 +8061,3,102000070,1 +8062,3,102000080,1 +8063,3,103000050,1 +8064,3,105000050,1 +8065,3,107000060,1 +8066,3,107000070,1 +8067,3,107000080,1 +8068,3,108000050,1 +8069,3,109000050,1 +8070,3,103000060,1 +8071,3,103000070,1 +8072,3,103000080,1 +8073,3,110000050,1 +8074,3,106000060,1 +8075,3,106000070,1 +8076,3,106000080,1 +8077,3,101000070,1 +8078,3,110000060,1 +8079,3,104000060,1 +8080,3,104000070,1 +8081,3,104000080,1 +8082,3,102000050,1 +8083,3,104000170,1 +8084,3,104000260,1 +8085,3,111000010,1 +8086,3,111000020,1 +8087,3,111000030,1 +8088,3,112000020,1 +8089,3,112000030,1 +8090,3,108000060,1 +8091,3,108000070,1 +8092,3,108000080,1 +8093,3,107000050,1 +8094,3,112000010,1 +8095,3,110000070,1 +8096,3,110000080,1 +8097,3,118000020,1 +8098,3,118000030,1 +8099,3,118000040,1 +8100,4,101000090,1 +8101,4,101000100,1 +8102,4,101000110,1 +8103,4,109000100,1 +8104,4,105000100,1 +8105,4,105000110,1 +8106,4,108000090,1 +8107,4,110000090,1 +8108,4,102000100,1 +8109,4,102000110,1 +8110,4,106000090,1 +8111,4,109000090,1 +8112,4,107000100,1 +8113,4,103000090,1 +8114,4,102000090,1 +8115,4,103000100,1 +8116,4,106000100,1 +8117,4,106000110,1 +8118,4,104000090,1 +8119,4,104000100,1 +8120,4,104000110,1 +8121,4,107000090,1 +8122,4,104000180,1 +8123,4,111000040,1 +8124,4,112000040,1 +8125,4,108000100,1 +8126,4,105000090,1 +8127,4,110000100,1 +8128,4,118000050,1 +8129,4,118000060,1 +8130,5,101000120,1 +8131,5,109000110,1 +8132,5,105000120,1 +8133,5,102000120,1 +8134,5,107000110,1 +8135,5,103000120,1 +8136,5,106000120,1 +8137,5,104000120,1 +8138,5,104000190,1 +8139,5,111000060,1 +8140,5,112000060,1 +8141,5,108000110,1 +8142,5,110000110,1 +8143,5,118000070,1 +8144,3,170004,3 +8145,3,180004,3 +8146,1,201000010,8 +8147,1,292000010,8 +8148,1,299000040,8 +8149,1,299000070,8 +8150,1,299000110,8 +8151,1,299000120,8 +8152,1,299000140,8 +8153,2,202000010,8 +8154,2,290000010,8 +8155,2,299000010,8 +8156,2,299000150,8 +8157,2,299000190,8 +8158,2,299000200,8 +8159,2,299000210,8 +8160,3,298000050,8 +8161,3,298000060,8 +8162,3,299000060,8 +8163,3,299000170,8 +8164,3,290000120,8 +8165,3,291000050,8 +8166,3,292000020,8 +8167,4,299000670,8 +8168,4,299000680,8 +8169,4,204000010,8 +8170,5,297000100,8 +8171,5,291000020,8 +8172,5,297000130,8 +8173,5,297000140,8 +8174,5,203000010,8 +8175,4,140000,3 +8176,5,150010,3 +8177,5,150020,3 +8178,5,150030,3 +8179,5,150040,3 +8180,5,190000,3 +8181,5,200000,3 +8182,5,210000,3 +8184,3,102000060,1 +8185,3,102000070,1 +8186,3,102000080,1 +8187,3,103000050,1 +8188,3,105000050,1 +8189,3,107000060,1 +8190,3,107000070,1 +8191,3,107000080,1 +8192,3,108000050,1 +8193,3,109000050,1 +8194,3,103000060,1 +8195,3,103000070,1 +8196,3,103000080,1 +8197,3,110000050,1 +8198,4,102000100,1 +8199,4,102000110,1 +8200,4,106000090,1 +8201,4,109000090,1 +8202,4,107000100,1 +8203,4,103000090,1 +8204,4,102000090,1 +8205,4,103000100,1 +8206,5,102000120,1 +8207,5,107000110,1 +8208,5,103000120,1 +8209,1,102000000,2 +8210,2,102000001,2 +8211,3,102000002,2 +8212,4,102000003,2 +8213,5,102000004,2 +8214,1,105000000,2 +8215,2,105000001,2 +8216,3,105000002,2 +8217,4,105000003,2 +8218,5,105000004,2 +8219,1,112000000,2 +8220,2,112000001,2 +8221,3,112000002,2 +8222,4,112000003,2 +8223,5,112000004,2 +8224,3,170004,3 +8225,4,170005,3 +8226,3,180004,3 +8227,4,180005,3 +8228,4,140000,3 +8229,4,150010,3 +8230,4,150020,3 +8231,4,150030,3 +8232,4,150040,3 +8234,3,118000020,1 +8235,3,118000030,1 +8236,3,118000040,1 +8237,3,106000060,1 +8238,3,106000070,1 +8239,3,106000080,1 +8240,3,101000070,1 +8241,3,110000060,1 +8242,3,104000060,1 +8243,3,104000070,1 +8244,3,104000080,1 +8245,3,102000050,1 +8246,3,104000170,1 +8247,3,104000260,1 +8248,4,118000050,1 +8249,4,118000060,1 +8250,4,106000100,1 +8251,4,106000110,1 +8252,4,104000090,1 +8253,4,104000100,1 +8254,4,104000110,1 +8255,4,104000270,1 +8256,4,107000090,1 +8257,4,104000180,1 +8258,5,118000070,1 +8259,5,106000120,1 +8260,5,104000120,1 +8261,5,104000190,1 +8262,1,103000000,2 +8263,2,103000001,2 +8264,3,103000002,2 +8265,4,103000003,2 +8266,5,103000004,2 +8267,1,111000000,2 +8268,2,111000001,2 +8269,3,111000002,2 +8270,4,111000003,2 +8271,5,111000004,2 +8272,1,115000000,2 +8273,2,115000001,2 +8274,3,115000002,2 +8275,4,115000003,2 +8276,5,115000004,2 +8277,4,110024,3 +8278,4,110034,3 +8279,4,110044,3 +8280,4,110054,3 +8281,3,110060,3 +8282,3,110070,3 +8283,3,110080,3 +8284,3,110090,3 +8285,3,110100,3 +8286,3,110110,3 +8287,3,110120,3 +8288,3,110130,3 +8289,3,110140,3 +8290,3,110150,3 +8291,3,110160,3 +8292,3,110170,3 +8293,3,110180,3 +8294,3,110190,3 +8295,3,110200,3 +8296,3,110210,3 +8297,3,110220,3 +8298,3,110230,3 +8299,3,110240,3 +8300,3,110250,3 +8301,3,110260,3 +8302,3,110270,3 +8303,3,110620,3 +8304,3,110670,3 +8305,4,140000,3 +8306,4,150010,3 +8307,4,150020,3 +8308,4,150030,3 +8309,4,150040,3 +8311,3,111000010,1 +8312,3,111000020,1 +8313,3,111000030,1 +8314,3,112000020,1 +8315,3,112000030,1 +8316,3,108000060,1 +8317,3,108000070,1 +8318,3,108000080,1 +8319,3,107000050,1 +8320,3,112000010,1 +8321,3,110000070,1 +8322,3,110000080,1 +8323,4,111000040,1 +8324,4,112000040,1 +8325,4,108000100,1 +8326,4,105000090,1 +8327,4,110000100,1 +8328,5,111000060,1 +8329,5,112000060,1 +8330,5,108000110,1 +8331,5,110000110,1 +8332,1,108000000,2 +8333,2,108000001,2 +8334,3,108000002,2 +8335,4,108000003,2 +8336,5,108000004,2 +8337,1,107000000,2 +8338,2,107000001,2 +8339,3,107000002,2 +8340,4,107000003,2 +8341,5,107000004,2 +8342,1,120000000,2 +8343,2,120000001,2 +8344,3,120000002,2 +8345,4,120000003,2 +8346,5,120000004,2 +8347,4,120024,3 +8348,4,120034,3 +8349,4,120044,3 +8350,4,120054,3 +8351,3,120241,3 +8352,3,120251,3 +8353,3,120261,3 +8354,3,120271,3 +8355,3,120300,3 +8356,3,120310,3 +8357,3,120320,3 +8358,3,120330,3 +8359,3,120340,3 +8360,3,120350,3 +8361,3,120360,3 +8362,3,120370,3 +8363,3,120380,3 +8364,3,120390,3 +8365,3,120400,3 +8366,3,120410,3 +8367,3,120420,3 +8368,3,120430,3 +8369,3,120450,3 +8370,3,120460,3 +8371,3,120550,3 +8372,3,120560,3 +8373,3,120570,3 +8374,3,120990,3 +8375,3,121000,3 +8376,3,121010,3 +8377,3,121020,3 +8378,4,140000,3 +8379,4,150010,3 +8380,4,150020,3 +8381,4,150030,3 +8382,4,150040,3 +8384,3,101000050,1 +8385,3,101000060,1 +8386,3,101000080,1 +8387,3,101000040,1 +8388,3,109000060,1 +8389,3,109000070,1 +8390,3,109000080,1 +8391,3,105000060,1 +8392,3,105000070,1 +8393,3,105000080,1 +8394,3,104000050,1 +8395,3,106000050,1 +8396,4,101000090,1 +8397,4,101000100,1 +8398,4,101000110,1 +8399,4,109000100,1 +8400,4,105000100,1 +8401,4,105000110,1 +8402,4,108000090,1 +8403,4,110000090,1 +8404,5,101000120,1 +8405,5,109000110,1 +8406,5,105000120,1 +8407,1,101000000,2 +8408,2,101000001,2 +8409,3,101000002,2 +8410,4,101000008,2 +8411,5,101000004,2 +8412,1,109000000,2 +8413,2,109000001,2 +8414,3,109000002,2 +8415,4,109000003,2 +8416,5,109000004,2 +8417,4,130024,3 +8418,4,130034,3 +8419,4,130044,3 +8420,4,130054,3 +8421,3,130060,3 +8422,3,130070,3 +8423,3,130080,3 +8424,3,130090,3 +8425,3,130100,3 +8426,3,130110,3 +8427,3,130120,3 +8428,3,130130,3 +8429,3,130140,3 +8430,3,130150,3 +8431,3,130160,3 +8432,3,130170,3 +8433,3,130180,3 +8434,3,130190,3 +8435,3,130200,3 +8436,3,130420,3 +8437,3,130510,3 +8438,3,130520,3 +8439,3,130531,3 +8440,3,130540,3 +8441,3,130660,3 +8442,3,130790,3 +8443,3,130800,3 +8444,3,131130,3 +8445,4,140000,3 +8446,4,150010,3 +8447,4,150020,3 +8448,4,150030,3 +8449,4,150040,3 +8451,1,101000000,2 +8452,2,101000001,2 +8453,3,101000002,2 +8454,4,101000008,2 +8455,5,101000004,2 +8456,1,102000000,2 +8457,2,102000001,2 +8458,3,102000002,2 +8459,4,102000003,2 +8460,5,102000004,2 +8461,1,103000000,2 +8462,2,103000001,2 +8463,3,103000002,2 +8464,4,103000003,2 +8465,5,103000004,2 +8466,1,105000000,2 +8467,2,105000001,2 +8468,3,105000002,2 +8469,4,105000003,2 +8470,5,105000004,2 +8471,1,108000000,2 +8472,2,108000001,2 +8473,3,108000002,2 +8474,4,108000003,2 +8475,5,108000004,2 +8476,1,109000000,2 +8477,2,109000001,2 +8478,3,109000002,2 +8479,4,109000003,2 +8480,5,109000004,2 +8481,1,111000000,2 +8482,2,111000001,2 +8483,3,111000002,2 +8484,4,111000003,2 +8485,5,111000004,2 +8486,1,112000000,2 +8487,2,112000001,2 +8488,3,112000002,2 +8489,4,112000003,2 +8490,5,112000004,2 +8491,1,120000000,2 +8492,2,120000001,2 +8493,3,120000002,2 +8494,4,120000003,2 +8495,5,120000004,2 +8496,1,101000010,1 +8497,2,101000020,1 +8498,2,101000030,1 +8499,3,101000040,1 +8500,3,101000050,1 +8501,3,101000060,1 +8502,3,101000070,1 +8503,3,101000080,1 +8504,1,102000010,1 +8505,2,102000020,1 +8506,2,102000030,1 +8507,2,102000040,1 +8508,3,102000050,1 +8509,3,102000060,1 +8510,3,102000070,1 +8511,3,102000080,1 +8512,1,103000010,1 +8513,2,103000020,1 +8514,2,103000030,1 +8515,2,103000040,1 +8516,3,103000050,1 +8517,3,103000060,1 +8518,3,103000070,1 +8519,3,103000080,1 +8520,1,104000010,1 +8521,2,104000020,1 +8522,2,104000030,1 +8523,2,104000040,1 +8524,3,104000050,1 +8525,3,104000060,1 +8526,3,104000070,1 +8527,3,104000080,1 +8528,1,105000010,1 +8529,2,105000020,1 +8530,2,105000030,1 +8531,2,105000040,1 +8532,3,105000050,1 +8533,3,105000060,1 +8534,3,105000070,1 +8535,3,105000080,1 +8536,1,106000010,1 +8537,2,106000020,1 +8538,2,106000030,1 +8539,2,106000040,1 +8540,3,106000050,1 +8541,3,106000060,1 +8542,3,106000070,1 +8543,3,106000080,1 +8544,1,107000010,1 +8545,2,107000020,1 +8546,2,107000030,1 +8547,2,107000040,1 +8548,3,107000050,1 +8549,3,107000060,1 +8550,3,107000070,1 +8551,3,107000080,1 +8552,1,108000010,1 +8553,2,108000020,1 +8554,2,108000030,1 +8555,2,108000040,1 +8556,3,108000050,1 +8557,3,108000060,1 +8558,3,108000070,1 +8559,3,108000080,1 +8560,2,180000,3 +8561,2,170002,3 +8562,3,170003,3 +8563,4,170004,3 +8565,1,101000000,2 +8566,2,101000001,2 +8567,3,101000002,2 +8568,4,101000008,2 +8569,5,101000004,2 +8570,1,102000000,2 +8571,2,102000001,2 +8572,3,102000002,2 +8573,4,102000003,2 +8574,5,102000004,2 +8575,1,103000000,2 +8576,2,103000001,2 +8577,3,103000002,2 +8578,4,103000003,2 +8579,5,103000004,2 +8580,1,105000000,2 +8581,2,105000001,2 +8582,3,105000002,2 +8583,4,105000003,2 +8584,5,105000004,2 +8585,1,108000000,2 +8586,2,108000001,2 +8587,3,108000002,2 +8588,4,108000003,2 +8589,5,108000004,2 +8590,1,109000000,2 +8591,2,109000001,2 +8592,3,109000002,2 +8593,4,109000003,2 +8594,5,109000004,2 +8595,1,111000000,2 +8596,2,111000001,2 +8597,3,111000002,2 +8598,4,111000003,2 +8599,5,111000004,2 +8600,1,112000000,2 +8601,2,112000001,2 +8602,3,112000002,2 +8603,4,112000003,2 +8604,5,112000004,2 +8605,1,120000000,2 +8606,2,120000001,2 +8607,3,120000002,2 +8608,4,120000003,2 +8609,5,120000004,2 +8610,1,101000010,1 +8611,2,101000020,1 +8612,2,101000030,1 +8613,3,101000040,1 +8614,3,101000050,1 +8615,3,101000060,1 +8616,3,101000070,1 +8617,3,101000080,1 +8618,1,102000010,1 +8619,2,102000020,1 +8620,2,102000030,1 +8621,2,102000040,1 +8622,3,102000050,1 +8623,3,102000060,1 +8624,3,102000070,1 +8625,3,102000080,1 +8626,1,103000010,1 +8627,2,103000020,1 +8628,2,103000030,1 +8629,2,103000040,1 +8630,3,103000050,1 +8631,3,103000060,1 +8632,3,103000070,1 +8633,3,103000080,1 +8634,1,104000010,1 +8635,2,104000020,1 +8636,2,104000030,1 +8637,2,104000040,1 +8638,3,104000050,1 +8639,3,104000060,1 +8640,3,104000070,1 +8641,3,104000080,1 +8642,1,105000010,1 +8643,2,105000020,1 +8644,2,105000030,1 +8645,2,105000040,1 +8646,3,105000050,1 +8647,3,105000060,1 +8648,3,105000070,1 +8649,3,105000080,1 +8650,1,106000010,1 +8651,2,106000020,1 +8652,2,106000030,1 +8653,2,106000040,1 +8654,3,106000050,1 +8655,3,106000060,1 +8656,3,106000070,1 +8657,3,106000080,1 +8658,1,107000010,1 +8659,2,107000020,1 +8660,2,107000030,1 +8661,2,107000040,1 +8662,3,107000050,1 +8663,3,107000060,1 +8664,3,107000070,1 +8665,3,107000080,1 +8666,1,108000010,1 +8667,2,108000020,1 +8668,2,108000030,1 +8669,2,108000040,1 +8670,3,108000050,1 +8671,3,108000060,1 +8672,3,108000070,1 +8673,3,108000080,1 +8674,2,180000,3 +8675,2,170002,3 +8676,3,170003,3 +8677,4,170004,3 +8679,1,101000000,2 +8680,2,101000001,2 +8681,3,101000002,2 +8682,4,101000008,2 +8683,5,101000004,2 +8684,1,102000000,2 +8685,2,102000001,2 +8686,3,102000002,2 +8687,4,102000003,2 +8688,5,102000004,2 +8689,1,103000000,2 +8690,2,103000001,2 +8691,3,103000002,2 +8692,4,103000003,2 +8693,5,103000004,2 +8694,1,105000000,2 +8695,2,105000001,2 +8696,3,105000002,2 +8697,4,105000003,2 +8698,5,105000004,2 +8699,1,108000000,2 +8700,2,108000001,2 +8701,3,108000002,2 +8702,4,108000003,2 +8703,5,108000004,2 +8704,1,109000000,2 +8705,2,109000001,2 +8706,3,109000002,2 +8707,4,109000003,2 +8708,5,109000004,2 +8709,1,111000000,2 +8710,2,111000001,2 +8711,3,111000002,2 +8712,4,111000003,2 +8713,5,111000004,2 +8714,1,112000000,2 +8715,2,112000001,2 +8716,3,112000002,2 +8717,4,112000003,2 +8718,5,112000004,2 +8719,1,120000000,2 +8720,2,120000001,2 +8721,3,120000002,2 +8722,4,120000003,2 +8723,5,120000004,2 +8724,1,101000010,1 +8725,2,101000020,1 +8726,2,101000030,1 +8727,3,101000040,1 +8728,3,101000050,1 +8729,3,101000060,1 +8730,3,101000070,1 +8731,3,101000080,1 +8732,1,102000010,1 +8733,2,102000020,1 +8734,2,102000030,1 +8735,2,102000040,1 +8736,3,102000050,1 +8737,3,102000060,1 +8738,3,102000070,1 +8739,3,102000080,1 +8740,1,103000010,1 +8741,2,103000020,1 +8742,2,103000030,1 +8743,2,103000040,1 +8744,3,103000050,1 +8745,3,103000060,1 +8746,3,103000070,1 +8747,3,103000080,1 +8748,1,104000010,1 +8749,2,104000020,1 +8750,2,104000030,1 +8751,2,104000040,1 +8752,3,104000050,1 +8753,3,104000060,1 +8754,3,104000070,1 +8755,3,104000080,1 +8756,1,105000010,1 +8757,2,105000020,1 +8758,2,105000030,1 +8759,2,105000040,1 +8760,3,105000050,1 +8761,3,105000060,1 +8762,3,105000070,1 +8763,3,105000080,1 +8764,1,106000010,1 +8765,2,106000020,1 +8766,2,106000030,1 +8767,2,106000040,1 +8768,3,106000050,1 +8769,3,106000060,1 +8770,3,106000070,1 +8771,3,106000080,1 +8772,1,107000010,1 +8773,2,107000020,1 +8774,2,107000030,1 +8775,2,107000040,1 +8776,3,107000050,1 +8777,3,107000060,1 +8778,3,107000070,1 +8779,3,107000080,1 +8780,1,108000010,1 +8781,2,108000020,1 +8782,2,108000030,1 +8783,2,108000040,1 +8784,3,108000050,1 +8785,3,108000060,1 +8786,3,108000070,1 +8787,3,108000080,1 +8788,2,180001,3 +8789,2,170002,3 +8790,3,170003,3 +8791,4,170004,3 +8793,1,101000000,2 +8794,2,101000001,2 +8795,3,101000002,2 +8796,4,101000008,2 +8797,5,101000004,2 +8798,1,102000000,2 +8799,2,102000001,2 +8800,3,102000002,2 +8801,4,102000003,2 +8802,5,102000004,2 +8803,1,103000000,2 +8804,2,103000001,2 +8805,3,103000002,2 +8806,4,103000003,2 +8807,5,103000004,2 +8808,1,105000000,2 +8809,2,105000001,2 +8810,3,105000002,2 +8811,4,105000003,2 +8812,5,105000004,2 +8813,1,108000000,2 +8814,2,108000001,2 +8815,3,108000002,2 +8816,4,108000003,2 +8817,5,108000004,2 +8818,1,109000000,2 +8819,2,109000001,2 +8820,3,109000002,2 +8821,4,109000003,2 +8822,5,109000004,2 +8823,1,111000000,2 +8824,2,111000001,2 +8825,3,111000002,2 +8826,4,111000003,2 +8827,5,111000004,2 +8828,1,112000000,2 +8829,2,112000001,2 +8830,3,112000002,2 +8831,4,112000003,2 +8832,5,112000004,2 +8833,1,120000000,2 +8834,2,120000001,2 +8835,3,120000002,2 +8836,4,120000003,2 +8837,5,120000004,2 +8838,1,101000010,1 +8839,2,101000020,1 +8840,2,101000030,1 +8841,3,101000040,1 +8842,3,101000050,1 +8843,3,101000060,1 +8844,3,101000070,1 +8845,3,101000080,1 +8846,1,102000010,1 +8847,2,102000020,1 +8848,2,102000030,1 +8849,2,102000040,1 +8850,3,102000050,1 +8851,3,102000060,1 +8852,3,102000070,1 +8853,3,102000080,1 +8854,1,103000010,1 +8855,2,103000020,1 +8856,2,103000030,1 +8857,2,103000040,1 +8858,3,103000050,1 +8859,3,103000060,1 +8860,3,103000070,1 +8861,3,103000080,1 +8862,1,104000010,1 +8863,2,104000020,1 +8864,2,104000030,1 +8865,2,104000040,1 +8866,3,104000050,1 +8867,3,104000060,1 +8868,3,104000070,1 +8869,3,104000080,1 +8870,1,105000010,1 +8871,2,105000020,1 +8872,2,105000030,1 +8873,2,105000040,1 +8874,3,105000050,1 +8875,3,105000060,1 +8876,3,105000070,1 +8877,3,105000080,1 +8878,1,106000010,1 +8879,2,106000020,1 +8880,2,106000030,1 +8881,2,106000040,1 +8882,3,106000050,1 +8883,3,106000060,1 +8884,3,106000070,1 +8885,3,106000080,1 +8886,1,107000010,1 +8887,2,107000020,1 +8888,2,107000030,1 +8889,2,107000040,1 +8890,3,107000050,1 +8891,3,107000060,1 +8892,3,107000070,1 +8893,3,107000080,1 +8894,1,108000010,1 +8895,2,108000020,1 +8896,2,108000030,1 +8897,2,108000040,1 +8898,3,108000050,1 +8899,3,108000060,1 +8900,3,108000070,1 +8901,3,108000080,1 +8902,1,109000010,1 +8903,2,109000020,1 +8904,2,109000030,1 +8905,2,109000040,1 +8906,3,109000050,1 +8907,3,109000060,1 +8908,3,109000070,1 +8909,3,109000080,1 +8910,2,180001,3 +8911,2,170002,3 +8912,3,170003,3 +8913,4,170004,3 +8915,1,101000000,2 +8916,2,101000001,2 +8917,3,101000002,2 +8918,4,101000008,2 +8919,5,101000004,2 +8920,1,102000000,2 +8921,2,102000001,2 +8922,3,102000002,2 +8923,4,102000003,2 +8924,5,102000004,2 +8925,1,103000000,2 +8926,2,103000001,2 +8927,3,103000002,2 +8928,4,103000003,2 +8929,5,103000004,2 +8930,1,105000000,2 +8931,2,105000001,2 +8932,3,105000002,2 +8933,4,105000003,2 +8934,5,105000004,2 +8935,1,107000000,2 +8936,2,107000001,2 +8937,3,107000002,2 +8938,4,107000003,2 +8939,5,107000004,2 +8940,1,108000000,2 +8941,2,108000001,2 +8942,3,108000002,2 +8943,4,108000003,2 +8944,5,108000004,2 +8945,1,109000000,2 +8946,2,109000001,2 +8947,3,109000002,2 +8948,4,109000003,2 +8949,5,109000004,2 +8950,1,111000000,2 +8951,2,111000001,2 +8952,3,111000002,2 +8953,4,111000003,2 +8954,5,111000004,2 +8955,1,112000000,2 +8956,2,112000001,2 +8957,3,112000002,2 +8958,4,112000003,2 +8959,5,112000004,2 +8960,1,120000000,2 +8961,2,120000001,2 +8962,3,120000002,2 +8963,4,120000003,2 +8964,5,120000004,2 +8965,1,101000010,1 +8966,2,101000020,1 +8967,2,101000030,1 +8968,3,101000040,1 +8969,3,101000050,1 +8970,3,101000060,1 +8971,3,101000070,1 +8972,3,101000080,1 +8973,1,102000010,1 +8974,2,102000020,1 +8975,2,102000030,1 +8976,2,102000040,1 +8977,3,102000050,1 +8978,3,102000060,1 +8979,3,102000070,1 +8980,3,102000080,1 +8981,1,103000010,1 +8982,2,103000020,1 +8983,2,103000030,1 +8984,2,103000040,1 +8985,3,103000050,1 +8986,3,103000060,1 +8987,3,103000070,1 +8988,3,103000080,1 +8989,1,104000010,1 +8990,2,104000020,1 +8991,2,104000030,1 +8992,2,104000040,1 +8993,3,104000050,1 +8994,3,104000060,1 +8995,3,104000070,1 +8996,3,104000080,1 +8997,1,105000010,1 +8998,2,105000020,1 +8999,2,105000030,1 +9000,2,105000040,1 +9001,3,105000050,1 +9002,3,105000060,1 +9003,3,105000070,1 +9004,3,105000080,1 +9005,1,106000010,1 +9006,2,106000020,1 +9007,2,106000030,1 +9008,2,106000040,1 +9009,3,106000050,1 +9010,3,106000060,1 +9011,3,106000070,1 +9012,3,106000080,1 +9013,1,107000010,1 +9014,2,107000020,1 +9015,2,107000030,1 +9016,2,107000040,1 +9017,3,107000050,1 +9018,3,107000060,1 +9019,3,107000070,1 +9020,3,107000080,1 +9021,1,108000010,1 +9022,2,108000020,1 +9023,2,108000030,1 +9024,2,108000040,1 +9025,3,108000050,1 +9026,3,108000060,1 +9027,3,108000070,1 +9028,3,108000080,1 +9029,1,109000010,1 +9030,2,109000020,1 +9031,2,109000030,1 +9032,2,109000040,1 +9033,3,109000050,1 +9034,3,109000060,1 +9035,3,109000070,1 +9036,3,109000080,1 +9037,1,110000010,1 +9038,2,110000020,1 +9039,2,110000030,1 +9040,2,110000040,1 +9041,3,110000050,1 +9042,3,110000060,1 +9043,3,110000070,1 +9044,3,110000080,1 +9045,2,180001,3 +9046,2,170002,3 +9047,3,170003,3 +9048,4,170004,3 +9050,1,101000010,1 +9051,1,102000010,1 +9052,1,103000010,1 +9053,1,104000010,1 +9054,1,105000010,1 +9055,1,106000010,1 +9056,1,107000010,1 +9057,1,108000010,1 +9058,1,109000010,1 +9059,1,110000010,1 +9060,2,101000020,1 +9061,2,101000030,1 +9062,2,102000020,1 +9063,2,102000030,1 +9064,2,102000040,1 +9065,2,103000020,1 +9066,2,103000030,1 +9067,2,103000040,1 +9068,2,104000020,1 +9069,2,104000030,1 +9070,2,104000040,1 +9071,2,105000020,1 +9072,2,105000030,1 +9073,2,105000040,1 +9074,2,106000020,1 +9075,2,106000030,1 +9076,2,106000040,1 +9077,2,107000020,1 +9078,2,107000030,1 +9079,2,107000040,1 +9080,2,108000020,1 +9081,2,108000030,1 +9082,2,108000040,1 +9083,2,109000020,1 +9084,2,109000030,1 +9085,2,109000040,1 +9086,2,110000020,1 +9087,2,110000030,1 +9088,2,110000040,1 +9089,2,118000010,1 +9090,3,101000050,1 +9091,3,101000060,1 +9092,3,101000080,1 +9093,3,101000040,1 +9094,3,109000060,1 +9095,3,109000070,1 +9096,3,109000080,1 +9097,3,105000060,1 +9098,3,105000070,1 +9099,3,105000080,1 +9100,3,104000050,1 +9101,3,106000050,1 +9102,3,102000060,1 +9103,3,102000070,1 +9104,3,102000080,1 +9105,3,103000050,1 +9106,3,105000050,1 +9107,3,107000060,1 +9108,3,107000070,1 +9109,3,107000080,1 +9110,3,108000050,1 +9111,3,109000050,1 +9112,3,103000060,1 +9113,3,103000070,1 +9114,3,103000080,1 +9115,3,110000050,1 +9116,3,106000060,1 +9117,3,106000070,1 +9118,3,106000080,1 +9119,3,101000070,1 +9120,3,110000060,1 +9121,3,104000060,1 +9122,3,104000070,1 +9123,3,104000080,1 +9124,3,102000050,1 +9125,3,104000170,1 +9126,3,104000260,1 +9127,3,111000010,1 +9128,3,111000020,1 +9129,3,111000030,1 +9130,3,112000020,1 +9131,3,112000030,1 +9132,3,108000060,1 +9133,3,108000070,1 +9134,3,108000080,1 +9135,3,107000050,1 +9136,3,112000010,1 +9137,3,110000070,1 +9138,3,110000080,1 +9139,3,118000020,1 +9140,3,118000030,1 +9141,3,118000040,1 +9142,4,101000090,1 +9143,4,101000100,1 +9144,4,101000110,1 +9145,4,109000100,1 +9146,4,105000100,1 +9147,4,105000110,1 +9148,4,108000090,1 +9149,4,110000090,1 +9150,4,102000100,1 +9151,4,102000110,1 +9152,4,106000090,1 +9153,4,109000090,1 +9154,4,107000100,1 +9155,4,103000090,1 +9156,4,102000090,1 +9157,4,103000100,1 +9158,4,106000100,1 +9159,4,106000110,1 +9160,4,104000090,1 +9161,4,104000100,1 +9162,4,104000110,1 +9163,4,107000090,1 +9164,4,104000180,1 +9165,4,111000040,1 +9166,4,112000040,1 +9167,4,108000100,1 +9168,4,105000090,1 +9169,4,110000100,1 +9170,4,118000050,1 +9171,4,118000060,1 +9172,5,101000120,1 +9173,5,109000110,1 +9174,5,105000120,1 +9175,5,102000120,1 +9176,5,107000110,1 +9177,5,103000120,1 +9178,5,106000120,1 +9179,5,104000120,1 +9180,5,104000190,1 +9181,5,111000060,1 +9182,5,112000060,1 +9183,5,108000110,1 +9184,5,110000110,1 +9185,5,118000070,1 +9186,1,201000010,8 +9187,1,292000010,8 +9188,1,299000040,8 +9189,1,299000070,8 +9190,1,299000110,8 +9191,1,299000120,8 +9192,1,299000140,8 +9193,2,202000010,8 +9194,2,290000010,8 +9195,2,299000010,8 +9196,2,299000150,8 +9197,2,299000190,8 +9198,2,299000200,8 +9199,2,299000210,8 +9200,3,298000050,8 +9201,3,298000060,8 +9202,3,299000060,8 +9203,3,299000170,8 +9204,3,290000120,8 +9205,3,291000050,8 +9206,3,292000020,8 +9207,4,299000670,8 +9208,4,299000680,8 +9209,4,204000010,8 +9210,4,209000040,8 +9211,5,297000100,8 +9212,5,291000020,8 +9213,5,297000130,8 +9214,5,297000140,8 +9215,5,203000010,8 +9216,5,206000030,8 +9217,1,170002,3 +9218,1,180002,3 +9219,2,170003,3 +9220,2,180003,3 +9221,3,170004,3 +9222,3,180004,3 +9223,4,140000,3 +9224,5,150010,3 +9225,5,150020,3 +9226,5,150030,3 +9227,5,150040,3 +9229,1,101000010,1 +9230,1,102000010,1 +9231,1,103000010,1 +9232,1,104000010,1 +9233,1,105000010,1 +9234,1,106000010,1 +9235,1,107000010,1 +9236,1,108000010,1 +9237,1,109000010,1 +9238,1,110000010,1 +9239,2,101000020,1 +9240,2,101000030,1 +9241,2,102000020,1 +9242,2,102000030,1 +9243,2,102000040,1 +9244,2,103000020,1 +9245,2,103000030,1 +9246,2,103000040,1 +9247,2,104000020,1 +9248,2,104000030,1 +9249,2,104000040,1 +9250,2,105000020,1 +9251,2,105000030,1 +9252,2,105000040,1 +9253,2,106000020,1 +9254,2,106000030,1 +9255,2,106000040,1 +9256,2,107000020,1 +9257,2,107000030,1 +9258,2,107000040,1 +9259,2,108000020,1 +9260,2,108000030,1 +9261,2,108000040,1 +9262,2,109000020,1 +9263,2,109000030,1 +9264,2,109000040,1 +9265,2,110000020,1 +9266,2,110000030,1 +9267,2,110000040,1 +9268,2,118000010,1 +9269,3,101000050,1 +9270,3,101000060,1 +9271,3,101000080,1 +9272,3,101000040,1 +9273,3,109000060,1 +9274,3,109000070,1 +9275,3,109000080,1 +9276,3,105000060,1 +9277,3,105000070,1 +9278,3,105000080,1 +9279,3,104000050,1 +9280,3,106000050,1 +9281,3,102000060,1 +9282,3,102000070,1 +9283,3,102000080,1 +9284,3,103000050,1 +9285,3,105000050,1 +9286,3,107000060,1 +9287,3,107000070,1 +9288,3,107000080,1 +9289,3,108000050,1 +9290,3,109000050,1 +9291,3,103000060,1 +9292,3,103000070,1 +9293,3,103000080,1 +9294,3,110000050,1 +9295,3,106000060,1 +9296,3,106000070,1 +9297,3,106000080,1 +9298,3,101000070,1 +9299,3,110000060,1 +9300,3,104000060,1 +9301,3,104000070,1 +9302,3,104000080,1 +9303,3,102000050,1 +9304,3,104000170,1 +9305,3,104000260,1 +9306,3,111000010,1 +9307,3,111000020,1 +9308,3,111000030,1 +9309,3,112000020,1 +9310,3,112000030,1 +9311,3,108000060,1 +9312,3,108000070,1 +9313,3,108000080,1 +9314,3,107000050,1 +9315,3,112000010,1 +9316,3,110000070,1 +9317,3,110000080,1 +9318,3,118000020,1 +9319,3,118000030,1 +9320,3,118000040,1 +9321,4,101000090,1 +9322,4,101000100,1 +9323,4,101000110,1 +9324,4,109000100,1 +9325,4,105000100,1 +9326,4,105000110,1 +9327,4,108000090,1 +9328,4,110000090,1 +9329,4,102000100,1 +9330,4,102000110,1 +9331,4,106000090,1 +9332,4,109000090,1 +9333,4,107000100,1 +9334,4,103000090,1 +9335,4,102000090,1 +9336,4,103000100,1 +9337,4,106000100,1 +9338,4,106000110,1 +9339,4,104000090,1 +9340,4,104000100,1 +9341,4,104000110,1 +9342,4,107000090,1 +9343,4,104000180,1 +9344,4,111000040,1 +9345,4,112000040,1 +9346,4,108000100,1 +9347,4,105000090,1 +9348,4,110000100,1 +9349,4,118000050,1 +9350,4,118000060,1 +9351,5,101000120,1 +9352,5,109000110,1 +9353,5,105000120,1 +9354,5,102000120,1 +9355,5,107000110,1 +9356,5,103000120,1 +9357,5,106000120,1 +9358,5,104000120,1 +9359,5,104000190,1 +9360,5,111000060,1 +9361,5,112000060,1 +9362,5,108000110,1 +9363,5,110000110,1 +9364,5,118000070,1 +9365,1,201000010,8 +9366,1,292000010,8 +9367,1,299000040,8 +9368,1,299000070,8 +9369,1,299000110,8 +9370,1,299000120,8 +9371,1,299000140,8 +9372,2,202000010,8 +9373,2,290000010,8 +9374,2,299000010,8 +9375,2,299000150,8 +9376,2,299000190,8 +9377,2,299000200,8 +9378,2,299000210,8 +9379,3,298000050,8 +9380,3,298000060,8 +9381,3,299000060,8 +9382,3,299000170,8 +9383,3,290000120,8 +9384,3,291000050,8 +9385,3,292000020,8 +9386,4,299000670,8 +9387,4,299000680,8 +9388,4,204000010,8 +9389,4,209000040,8 +9390,5,297000100,8 +9391,5,291000020,8 +9392,5,297000130,8 +9393,5,297000140,8 +9394,5,203000010,8 +9395,5,206000030,8 +9396,2,170003,3 +9397,2,180003,3 +9398,3,170004,3 +9399,3,180004,3 +9400,4,140000,3 +9401,5,150010,3 +9402,5,150020,3 +9403,5,150030,3 +9404,5,150040,3 +9406,3,101000050,1 +9407,3,101000060,1 +9408,3,101000080,1 +9409,3,101000040,1 +9410,3,109000060,1 +9411,3,109000070,1 +9412,3,109000080,1 +9413,3,105000060,1 +9414,3,105000070,1 +9415,3,105000080,1 +9416,3,104000050,1 +9417,3,106000050,1 +9418,3,102000060,1 +9419,3,102000070,1 +9420,3,102000080,1 +9421,3,103000050,1 +9422,3,105000050,1 +9423,3,107000060,1 +9424,3,107000070,1 +9425,3,107000080,1 +9426,3,108000050,1 +9427,3,109000050,1 +9428,3,103000060,1 +9429,3,103000070,1 +9430,3,103000080,1 +9431,3,110000050,1 +9432,3,106000060,1 +9433,3,106000070,1 +9434,3,106000080,1 +9435,3,101000070,1 +9436,3,110000060,1 +9437,3,104000060,1 +9438,3,104000070,1 +9439,3,104000080,1 +9440,3,102000050,1 +9441,3,104000170,1 +9442,3,104000260,1 +9443,3,111000010,1 +9444,3,111000020,1 +9445,3,111000030,1 +9446,3,112000020,1 +9447,3,112000030,1 +9448,3,108000060,1 +9449,3,108000070,1 +9450,3,108000080,1 +9451,3,107000050,1 +9452,3,112000010,1 +9453,3,110000070,1 +9454,3,110000080,1 +9455,3,118000020,1 +9456,3,118000030,1 +9457,3,118000040,1 +9458,4,101000090,1 +9459,4,101000100,1 +9460,4,101000110,1 +9461,4,109000100,1 +9462,4,105000100,1 +9463,4,105000110,1 +9464,4,108000090,1 +9465,4,110000090,1 +9466,4,102000100,1 +9467,4,102000110,1 +9468,4,106000090,1 +9469,4,109000090,1 +9470,4,107000100,1 +9471,4,103000090,1 +9472,4,102000090,1 +9473,4,103000100,1 +9474,4,106000100,1 +9475,4,106000110,1 +9476,4,104000090,1 +9477,4,104000100,1 +9478,4,104000110,1 +9479,4,107000090,1 +9480,4,104000180,1 +9481,4,111000040,1 +9482,4,112000040,1 +9483,4,108000100,1 +9484,4,105000090,1 +9485,4,110000100,1 +9486,4,118000050,1 +9487,4,118000060,1 +9488,5,101000120,1 +9489,5,109000110,1 +9490,5,105000120,1 +9491,5,102000120,1 +9492,5,107000110,1 +9493,5,103000120,1 +9494,5,106000120,1 +9495,5,104000120,1 +9496,5,104000190,1 +9497,5,111000060,1 +9498,5,112000060,1 +9499,5,108000110,1 +9500,5,110000110,1 +9501,5,118000070,1 +9502,1,201000010,8 +9503,1,292000010,8 +9504,1,299000040,8 +9505,1,299000070,8 +9506,1,299000110,8 +9507,1,299000120,8 +9508,1,299000140,8 +9509,2,202000010,8 +9510,2,290000010,8 +9511,2,299000010,8 +9512,2,299000150,8 +9513,2,299000190,8 +9514,2,299000200,8 +9515,2,299000210,8 +9516,3,298000050,8 +9517,3,298000060,8 +9518,3,299000060,8 +9519,3,299000170,8 +9520,3,290000120,8 +9521,3,291000050,8 +9522,3,292000020,8 +9523,4,299000670,8 +9524,4,299000680,8 +9525,4,204000010,8 +9526,4,209000040,8 +9527,5,297000100,8 +9528,5,291000020,8 +9529,5,297000130,8 +9530,5,297000140,8 +9531,5,203000010,8 +9532,5,206000030,8 +9533,3,170004,3 +9534,3,180004,3 +9535,4,140000,3 +9536,5,150010,3 +9537,5,150020,3 +9538,5,150030,3 +9539,5,150040,3 +9541,3,101000050,1 +9542,3,101000060,1 +9543,3,101000080,1 +9544,3,101000040,1 +9545,3,109000060,1 +9546,3,109000070,1 +9547,3,109000080,1 +9548,3,105000060,1 +9549,3,105000070,1 +9550,3,105000080,1 +9551,3,104000050,1 +9552,3,106000050,1 +9553,3,102000060,1 +9554,3,102000070,1 +9555,3,102000080,1 +9556,3,103000050,1 +9557,3,105000050,1 +9558,3,107000060,1 +9559,3,107000070,1 +9560,3,107000080,1 +9561,3,108000050,1 +9562,3,109000050,1 +9563,3,103000060,1 +9564,3,103000070,1 +9565,3,103000080,1 +9566,3,110000050,1 +9567,3,106000060,1 +9568,3,106000070,1 +9569,3,106000080,1 +9570,3,101000070,1 +9571,3,110000060,1 +9572,3,104000060,1 +9573,3,104000070,1 +9574,3,104000080,1 +9575,3,102000050,1 +9576,3,104000170,1 +9577,3,104000260,1 +9578,3,111000010,1 +9579,3,111000020,1 +9580,3,111000030,1 +9581,3,112000020,1 +9582,3,112000030,1 +9583,3,108000060,1 +9584,3,108000070,1 +9585,3,108000080,1 +9586,3,107000050,1 +9587,3,112000010,1 +9588,3,110000070,1 +9589,3,110000080,1 +9590,3,118000020,1 +9591,3,118000030,1 +9592,3,118000040,1 +9593,4,101000090,1 +9594,4,101000100,1 +9595,4,101000110,1 +9596,4,109000100,1 +9597,4,105000100,1 +9598,4,105000110,1 +9599,4,108000090,1 +9600,4,110000090,1 +9601,4,102000100,1 +9602,4,102000110,1 +9603,4,106000090,1 +9604,4,109000090,1 +9605,4,107000100,1 +9606,4,103000090,1 +9607,4,102000090,1 +9608,4,103000100,1 +9609,4,106000100,1 +9610,4,106000110,1 +9611,4,104000090,1 +9612,4,104000100,1 +9613,4,104000110,1 +9614,4,107000090,1 +9615,4,104000180,1 +9616,4,111000040,1 +9617,4,112000040,1 +9618,4,108000100,1 +9619,4,105000090,1 +9620,4,110000100,1 +9621,4,118000050,1 +9622,4,118000060,1 +9623,5,101000120,1 +9624,5,109000110,1 +9625,5,105000120,1 +9626,5,102000120,1 +9627,5,107000110,1 +9628,5,103000120,1 +9629,5,106000120,1 +9630,5,104000120,1 +9631,5,104000190,1 +9632,5,111000060,1 +9633,5,112000060,1 +9634,5,108000110,1 +9635,5,110000110,1 +9636,5,118000070,1 +9637,1,201000010,8 +9638,1,292000010,8 +9639,1,299000040,8 +9640,1,299000070,8 +9641,1,299000110,8 +9642,1,299000120,8 +9643,1,299000140,8 +9644,2,202000010,8 +9645,2,290000010,8 +9646,2,299000010,8 +9647,2,299000150,8 +9648,2,299000190,8 +9649,2,299000200,8 +9650,2,299000210,8 +9651,3,298000050,8 +9652,3,298000060,8 +9653,3,299000060,8 +9654,3,299000170,8 +9655,3,290000120,8 +9656,3,291000050,8 +9657,3,292000020,8 +9658,4,299000670,8 +9659,4,299000680,8 +9660,4,204000010,8 +9661,4,209000040,8 +9662,5,297000100,8 +9663,5,291000020,8 +9664,5,297000130,8 +9665,5,297000140,8 +9666,5,203000010,8 +9667,5,206000030,8 +9668,3,170004,3 +9669,3,180004,3 +9670,4,140000,3 +9671,5,150010,3 +9672,5,150020,3 +9673,5,150030,3 +9674,5,150040,3 +9676,3,101000050,1 +9677,3,101000060,1 +9678,3,101000080,1 +9679,3,101000040,1 +9680,3,109000060,1 +9681,3,109000070,1 +9682,3,109000080,1 +9683,3,105000060,1 +9684,3,105000070,1 +9685,3,105000080,1 +9686,3,104000050,1 +9687,3,106000050,1 +9688,3,102000060,1 +9689,3,102000070,1 +9690,3,102000080,1 +9691,3,103000050,1 +9692,3,105000050,1 +9693,3,107000060,1 +9694,3,107000070,1 +9695,3,107000080,1 +9696,3,108000050,1 +9697,3,109000050,1 +9698,3,103000060,1 +9699,3,103000070,1 +9700,3,103000080,1 +9701,3,110000050,1 +9702,3,106000060,1 +9703,3,106000070,1 +9704,3,106000080,1 +9705,3,101000070,1 +9706,3,110000060,1 +9707,3,104000060,1 +9708,3,104000070,1 +9709,3,104000080,1 +9710,3,102000050,1 +9711,3,104000170,1 +9712,3,104000260,1 +9713,3,111000010,1 +9714,3,111000020,1 +9715,3,111000030,1 +9716,3,112000020,1 +9717,3,112000030,1 +9718,3,108000060,1 +9719,3,108000070,1 +9720,3,108000080,1 +9721,3,107000050,1 +9722,3,112000010,1 +9723,3,110000070,1 +9724,3,110000080,1 +9725,3,118000020,1 +9726,3,118000030,1 +9727,3,118000040,1 +9728,4,101000090,1 +9729,4,101000100,1 +9730,4,101000110,1 +9731,4,109000100,1 +9732,4,105000100,1 +9733,4,105000110,1 +9734,4,108000090,1 +9735,4,110000090,1 +9736,4,102000100,1 +9737,4,102000110,1 +9738,4,106000090,1 +9739,4,109000090,1 +9740,4,107000100,1 +9741,4,103000090,1 +9742,4,102000090,1 +9743,4,103000100,1 +9744,4,106000100,1 +9745,4,106000110,1 +9746,4,104000090,1 +9747,4,104000100,1 +9748,4,104000110,1 +9749,4,107000090,1 +9750,4,104000180,1 +9751,4,111000040,1 +9752,4,112000040,1 +9753,4,108000100,1 +9754,4,105000090,1 +9755,4,110000100,1 +9756,4,118000050,1 +9757,4,118000060,1 +9758,5,101000120,1 +9759,5,109000110,1 +9760,5,105000120,1 +9761,5,102000120,1 +9762,5,107000110,1 +9763,5,103000120,1 +9764,5,106000120,1 +9765,5,104000120,1 +9766,5,104000190,1 +9767,5,111000060,1 +9768,5,112000060,1 +9769,5,108000110,1 +9770,5,110000110,1 +9771,5,118000070,1 +9772,1,201000010,8 +9773,1,292000010,8 +9774,1,299000040,8 +9775,1,299000070,8 +9776,1,299000110,8 +9777,1,299000120,8 +9778,1,299000140,8 +9779,2,202000010,8 +9780,2,290000010,8 +9781,2,299000010,8 +9782,2,299000150,8 +9783,2,299000190,8 +9784,2,299000200,8 +9785,2,299000210,8 +9786,3,298000050,8 +9787,3,298000060,8 +9788,3,299000060,8 +9789,3,299000170,8 +9790,3,290000120,8 +9791,3,291000050,8 +9792,3,292000020,8 +9793,4,299000670,8 +9794,4,299000680,8 +9795,4,204000010,8 +9796,4,209000040,8 +9797,5,297000100,8 +9798,5,291000020,8 +9799,5,297000130,8 +9800,5,297000140,8 +9801,5,203000010,8 +9802,5,206000030,8 +9803,3,170004,3 +9804,3,180004,3 +9805,4,140000,3 +9806,5,150010,3 +9807,5,150020,3 +9808,5,150030,3 +9809,5,150040,3 +9810,5,190000,3 +9811,5,200000,3 +9812,5,210000,3 +9814,3,101000050,1 +9815,3,101000060,1 +9816,3,101000080,1 +9817,3,101000040,1 +9818,3,109000060,1 +9819,3,109000070,1 +9820,3,109000080,1 +9821,3,105000060,1 +9822,3,105000070,1 +9823,3,105000080,1 +9824,3,104000050,1 +9825,3,106000050,1 +9826,4,101000090,1 +9827,4,101000100,1 +9828,4,101000110,1 +9829,4,109000100,1 +9830,4,105000100,1 +9831,4,105000110,1 +9832,4,108000090,1 +9833,4,110000090,1 +9834,5,101000120,1 +9835,5,109000110,1 +9836,5,105000120,1 +9837,1,101000000,2 +9838,2,101000001,2 +9839,3,101000002,2 +9840,4,101000008,2 +9841,5,101000004,2 +9842,1,109000000,2 +9843,2,109000001,2 +9844,3,109000002,2 +9845,4,109000003,2 +9846,5,109000004,2 +9847,3,170004,3 +9848,4,170005,3 +9849,3,180004,3 +9850,4,180005,3 +9851,4,140000,3 +9852,4,150010,3 +9853,4,150020,3 +9854,4,150030,3 +9855,4,150040,3 +9857,3,102000060,1 +9858,3,102000070,1 +9859,3,102000080,1 +9860,3,103000050,1 +9861,3,105000050,1 +9862,3,107000060,1 +9863,3,107000070,1 +9864,3,107000080,1 +9865,3,108000050,1 +9866,3,109000050,1 +9867,3,103000060,1 +9868,3,103000070,1 +9869,3,103000080,1 +9870,3,110000050,1 +9871,4,102000100,1 +9872,4,102000350,1 +9873,4,102000110,1 +9874,4,106000090,1 +9875,4,109000090,1 +9876,4,107000100,1 +9877,4,103000090,1 +9878,4,102000090,1 +9879,4,103000100,1 +9880,5,102000120,1 +9881,5,107000110,1 +9882,5,103000120,1 +9883,1,102000000,2 +9884,2,102000001,2 +9885,3,102000002,2 +9886,4,102000003,2 +9887,5,102000004,2 +9888,1,105000000,2 +9889,2,105000001,2 +9890,3,105000002,2 +9891,4,105000003,2 +9892,5,105000004,2 +9893,1,112000000,2 +9894,2,112000001,2 +9895,3,112000002,2 +9896,4,112000003,2 +9897,5,112000004,2 +9898,4,110024,3 +9899,4,110034,3 +9900,4,110044,3 +9901,4,110054,3 +9902,3,110060,3 +9903,3,110070,3 +9904,3,110080,3 +9905,3,110090,3 +9906,3,110100,3 +9907,3,110110,3 +9908,3,110120,3 +9909,3,110130,3 +9910,3,110140,3 +9911,3,110150,3 +9912,3,110160,3 +9913,3,110170,3 +9914,3,110180,3 +9915,3,110190,3 +9916,3,110200,3 +9917,3,110210,3 +9918,3,110220,3 +9919,3,110230,3 +9920,3,110240,3 +9921,3,110250,3 +9922,3,110260,3 +9923,3,110270,3 +9924,3,110620,3 +9925,3,110670,3 +9926,4,140000,3 +9927,4,150010,3 +9928,4,150020,3 +9929,4,150030,3 +9930,4,150040,3 +9932,3,118000020,1 +9933,3,118000030,1 +9934,3,118000040,1 +9935,3,106000060,1 +9936,3,106000070,1 +9937,3,106000080,1 +9938,3,101000070,1 +9939,3,110000060,1 +9940,3,104000060,1 +9941,3,104000070,1 +9942,3,104000080,1 +9943,3,102000050,1 +9944,3,104000170,1 +9945,3,104000260,1 +9946,4,118000050,1 +9947,4,118000060,1 +9948,4,106000100,1 +9949,4,106000110,1 +9950,4,104000090,1 +9951,4,104000100,1 +9952,4,104000110,1 +9953,4,104000270,1 +9954,4,107000090,1 +9955,4,104000180,1 +9956,5,118000070,1 +9957,5,106000120,1 +9958,5,104000120,1 +9959,5,104000190,1 +9960,1,103000000,2 +9961,2,103000001,2 +9962,3,103000002,2 +9963,4,103000003,2 +9964,5,103000004,2 +9965,1,111000000,2 +9966,2,111000001,2 +9967,3,111000002,2 +9968,4,111000003,2 +9969,5,111000004,2 +9970,1,115000000,2 +9971,2,115000001,2 +9972,3,115000002,2 +9973,4,115000003,2 +9974,5,115000004,2 +9975,4,120024,3 +9976,4,120034,3 +9977,4,120044,3 +9978,4,120054,3 +9979,3,120241,3 +9980,3,120251,3 +9981,3,120261,3 +9982,3,120271,3 +9983,3,120300,3 +9984,3,120310,3 +9985,3,120320,3 +9986,3,120330,3 +9987,3,120340,3 +9988,3,120350,3 +9989,3,120360,3 +9990,3,120370,3 +9991,3,120380,3 +9992,3,120390,3 +9993,3,120400,3 +9994,3,120410,3 +9995,3,120420,3 +9996,3,120430,3 +9997,3,120450,3 +9998,3,120460,3 +9999,3,120550,3 +10000,3,120560,3 +10001,3,120570,3 +10002,3,120990,3 +10003,3,121000,3 +10004,3,121010,3 +10005,3,121020,3 +10006,4,140000,3 +10007,4,150010,3 +10008,4,150020,3 +10009,4,150030,3 +10010,4,150040,3 +10012,3,111000010,1 +10013,3,111000020,1 +10014,3,111000030,1 +10015,3,112000020,1 +10016,3,112000030,1 +10017,3,108000060,1 +10018,3,108000070,1 +10019,3,108000080,1 +10020,3,107000050,1 +10021,3,112000010,1 +10022,3,110000070,1 +10023,3,110000080,1 +10024,4,111000040,1 +10025,4,112000040,1 +10026,4,108000100,1 +10027,4,105000090,1 +10028,4,110000100,1 +10029,5,111000060,1 +10030,5,112000060,1 +10031,5,108000110,1 +10032,5,110000110,1 +10033,1,108000000,2 +10034,2,108000001,2 +10035,3,108000002,2 +10036,4,108000003,2 +10037,5,108000004,2 +10038,1,107000000,2 +10039,2,107000001,2 +10040,3,107000002,2 +10041,4,107000003,2 +10042,5,107000004,2 +10043,1,120000000,2 +10044,2,120000001,2 +10045,3,120000002,2 +10046,4,120000003,2 +10047,5,120000004,2 +10048,4,130024,3 +10049,4,130034,3 +10050,4,130044,3 +10051,4,130054,3 +10052,3,130060,3 +10053,3,130070,3 +10054,3,130080,3 +10055,3,130090,3 +10056,3,130100,3 +10057,3,130110,3 +10058,3,130120,3 +10059,3,130130,3 +10060,3,130140,3 +10061,3,130150,3 +10062,3,130160,3 +10063,3,130170,3 +10064,3,130180,3 +10065,3,130190,3 +10066,3,130200,3 +10067,3,130420,3 +10068,3,130510,3 +10069,3,130520,3 +10070,3,130531,3 +10071,3,130540,3 +10072,3,130660,3 +10073,3,130790,3 +10074,3,130800,3 +10075,3,131130,3 +10076,4,140000,3 +10077,4,150010,3 +10078,4,150020,3 +10079,4,150030,3 +10080,4,150040,3 +10082,3,111000010,1 +10083,3,111000020,1 +10084,3,111000030,1 +10085,3,112000020,1 +10086,3,112000030,1 +10087,3,108000060,1 +10088,3,108000070,1 +10089,3,108000080,1 +10090,3,107000050,1 +10091,3,112000010,1 +10092,3,110000070,1 +10093,3,110000080,1 +10094,4,111000040,1 +10095,4,112000040,1 +10096,4,108000100,1 +10097,4,105000090,1 +10098,4,110000100,1 +10099,5,111000060,1 +10100,5,112000060,1 +10101,5,108000110,1 +10102,5,110000110,1 +10103,1,108000000,2 +10104,2,108000001,2 +10105,3,108000002,2 +10106,4,108000003,2 +10107,5,108000004,2 +10108,1,107000000,2 +10109,2,107000001,2 +10110,3,107000002,2 +10111,4,107000003,2 +10112,5,107000004,2 +10113,1,120000000,2 +10114,2,120000001,2 +10115,3,120000002,2 +10116,4,120000003,2 +10117,5,120000004,2 +10118,3,170004,3 +10119,4,170005,3 +10120,3,180004,3 +10121,4,180005,3 +10122,4,140000,3 +10123,4,150010,3 +10124,4,150020,3 +10125,4,150030,3 +10126,4,150040,3 +10128,3,101000050,1 +10129,3,101000060,1 +10130,3,101000080,1 +10131,3,101000040,1 +10132,3,109000060,1 +10133,3,109000070,1 +10134,3,109000080,1 +10135,3,105000060,1 +10136,3,105000070,1 +10137,3,105000080,1 +10138,3,104000050,1 +10139,3,106000050,1 +10140,4,101000090,1 +10141,4,101000100,1 +10142,4,101000110,1 +10143,4,109000100,1 +10144,4,105000100,1 +10145,4,105000110,1 +10146,4,108000090,1 +10147,4,110000090,1 +10148,5,101000120,1 +10149,5,109000110,1 +10150,5,105000120,1 +10151,1,101000000,2 +10152,2,101000001,2 +10153,3,101000002,2 +10154,4,101000008,2 +10155,5,101000004,2 +10156,1,109000000,2 +10157,2,109000001,2 +10158,3,109000002,2 +10159,4,109000003,2 +10160,5,109000004,2 +10161,4,110024,3 +10162,4,110034,3 +10163,4,110044,3 +10164,4,110054,3 +10165,3,110060,3 +10166,3,110070,3 +10167,3,110080,3 +10168,3,110090,3 +10169,3,110100,3 +10170,3,110110,3 +10171,3,110120,3 +10172,3,110130,3 +10173,3,110140,3 +10174,3,110150,3 +10175,3,110160,3 +10176,3,110170,3 +10177,3,110180,3 +10178,3,110190,3 +10179,3,110200,3 +10180,3,110210,3 +10181,3,110220,3 +10182,3,110230,3 +10183,3,110240,3 +10184,3,110250,3 +10185,3,110260,3 +10186,3,110270,3 +10187,3,110620,3 +10188,3,110670,3 +10189,4,140000,3 +10190,4,150010,3 +10191,4,150020,3 +10192,4,150030,3 +10193,4,150040,3 +10195,3,102000060,1 +10196,3,102000070,1 +10197,3,102000080,1 +10198,3,103000050,1 +10199,3,105000050,1 +10200,3,107000060,1 +10201,3,107000070,1 +10202,3,107000080,1 +10203,3,108000050,1 +10204,3,109000050,1 +10205,3,103000060,1 +10206,3,103000070,1 +10207,3,103000080,1 +10208,3,110000050,1 +10209,4,102000100,1 +10210,4,102000110,1 +10211,4,102000350,1 +10212,4,106000090,1 +10213,4,109000090,1 +10214,4,107000100,1 +10215,4,103000090,1 +10216,4,102000090,1 +10217,4,103000100,1 +10218,5,102000120,1 +10219,5,107000110,1 +10220,5,103000120,1 +10221,1,102000000,2 +10222,2,102000001,2 +10223,3,102000002,2 +10224,4,102000003,2 +10225,5,102000004,2 +10226,1,105000000,2 +10227,2,105000001,2 +10228,3,105000002,2 +10229,4,105000003,2 +10230,5,105000004,2 +10231,1,112000000,2 +10232,2,112000001,2 +10233,3,112000002,2 +10234,4,112000003,2 +10235,5,112000004,2 +10236,4,120024,3 +10237,4,120034,3 +10238,4,120044,3 +10239,4,120054,3 +10240,3,120241,3 +10241,3,120251,3 +10242,3,120261,3 +10243,3,120271,3 +10244,3,120300,3 +10245,3,120310,3 +10246,3,120320,3 +10247,3,120330,3 +10248,3,120340,3 +10249,3,120350,3 +10250,3,120360,3 +10251,3,120370,3 +10252,3,120380,3 +10253,3,120390,3 +10254,3,120400,3 +10255,3,120410,3 +10256,3,120420,3 +10257,3,120430,3 +10258,3,120450,3 +10259,3,120460,3 +10260,3,120550,3 +10261,3,120560,3 +10262,3,120570,3 +10263,3,120990,3 +10264,3,121000,3 +10265,3,121010,3 +10266,3,121020,3 +10267,3,121100,3 +10268,4,140000,3 +10269,4,150010,3 +10270,4,150020,3 +10271,4,150030,3 +10272,4,150040,3 +10274,3,118000020,1 +10275,3,118000030,1 +10276,3,118000040,1 +10277,3,106000060,1 +10278,3,106000070,1 +10279,3,106000080,1 +10280,3,101000070,1 +10281,3,110000060,1 +10282,3,104000060,1 +10283,3,104000070,1 +10284,3,104000080,1 +10285,3,102000050,1 +10286,3,104000170,1 +10287,3,104000260,1 +10288,4,118000050,1 +10289,4,118000060,1 +10290,4,106000100,1 +10291,4,106000110,1 +10292,4,104000090,1 +10293,4,104000100,1 +10294,4,104000110,1 +10295,4,104000270,1 +10296,4,107000090,1 +10297,4,104000180,1 +10298,5,118000070,1 +10299,5,106000120,1 +10300,5,104000120,1 +10301,5,104000190,1 +10302,1,103000000,2 +10303,2,103000001,2 +10304,3,103000002,2 +10305,4,103000003,2 +10306,5,103000004,2 +10307,1,111000000,2 +10308,2,111000001,2 +10309,3,111000002,2 +10310,4,111000003,2 +10311,5,111000004,2 +10312,1,115000000,2 +10313,2,115000001,2 +10314,3,115000002,2 +10315,4,115000003,2 +10316,5,115000004,2 +10317,4,130024,3 +10318,4,130034,3 +10319,4,130044,3 +10320,4,130054,3 +10321,3,130060,3 +10322,3,130070,3 +10323,3,130080,3 +10324,3,130090,3 +10325,3,130100,3 +10326,3,130110,3 +10327,3,130120,3 +10328,3,130130,3 +10329,3,130140,3 +10330,3,130150,3 +10331,3,130160,3 +10332,3,130170,3 +10333,3,130180,3 +10334,3,130190,3 +10335,3,130200,3 +10336,3,130420,3 +10337,3,130510,3 +10338,3,130520,3 +10339,3,130531,3 +10340,3,130540,3 +10341,3,130660,3 +10342,3,130790,3 +10343,3,130800,3 +10344,3,131130,3 +10345,4,140000,3 +10346,4,150010,3 +10347,4,150020,3 +10348,4,150030,3 +10349,4,150040,3 +10351,1,101000010,1 +10352,1,102000010,1 +10353,1,103000010,1 +10354,1,104000010,1 +10355,1,105000010,1 +10356,1,106000010,1 +10357,1,107000010,1 +10358,1,108000010,1 +10359,1,109000010,1 +10360,1,110000010,1 +10361,2,101000020,1 +10362,2,101000030,1 +10363,2,102000020,1 +10364,2,102000030,1 +10365,2,102000040,1 +10366,2,103000020,1 +10367,2,103000030,1 +10368,2,103000040,1 +10369,2,104000020,1 +10370,2,104000030,1 +10371,2,104000040,1 +10372,2,105000020,1 +10373,2,105000030,1 +10374,2,105000040,1 +10375,2,106000020,1 +10376,2,106000030,1 +10377,2,106000040,1 +10378,2,107000020,1 +10379,2,107000030,1 +10380,2,107000040,1 +10381,2,108000020,1 +10382,2,108000030,1 +10383,2,108000040,1 +10384,2,109000020,1 +10385,2,109000030,1 +10386,2,109000040,1 +10387,2,110000020,1 +10388,2,110000030,1 +10389,2,110000040,1 +10390,2,118000010,1 +10391,3,101000050,1 +10392,3,101000060,1 +10393,3,101000080,1 +10394,3,101000040,1 +10395,3,109000060,1 +10396,3,109000070,1 +10397,3,109000080,1 +10398,3,105000060,1 +10399,3,105000070,1 +10400,3,105000080,1 +10401,3,104000050,1 +10402,3,106000050,1 +10403,3,102000060,1 +10404,3,102000070,1 +10405,3,102000080,1 +10406,3,103000050,1 +10407,3,105000050,1 +10408,3,107000060,1 +10409,3,107000070,1 +10410,3,107000080,1 +10411,3,108000050,1 +10412,3,109000050,1 +10413,3,103000060,1 +10414,3,103000070,1 +10415,3,103000080,1 +10416,3,110000050,1 +10417,3,106000060,1 +10418,3,106000070,1 +10419,3,106000080,1 +10420,3,101000070,1 +10421,3,110000060,1 +10422,3,104000060,1 +10423,3,104000070,1 +10424,3,104000080,1 +10425,3,102000050,1 +10426,3,104000170,1 +10427,3,104000260,1 +10428,3,111000010,1 +10429,3,111000020,1 +10430,3,111000030,1 +10431,3,112000020,1 +10432,3,112000030,1 +10433,3,108000060,1 +10434,3,108000070,1 +10435,3,108000080,1 +10436,3,107000050,1 +10437,3,112000010,1 +10438,3,110000070,1 +10439,3,110000080,1 +10440,3,118000020,1 +10441,3,118000030,1 +10442,3,118000040,1 +10443,4,101000090,1 +10444,4,101000100,1 +10445,4,101000110,1 +10446,4,109000100,1 +10447,4,105000100,1 +10448,4,105000110,1 +10449,4,108000090,1 +10450,4,110000090,1 +10451,4,102000100,1 +10452,4,102000110,1 +10453,4,106000090,1 +10454,4,109000090,1 +10455,4,107000100,1 +10456,4,103000090,1 +10457,4,102000090,1 +10458,4,103000100,1 +10459,4,106000100,1 +10460,4,106000110,1 +10461,4,104000090,1 +10462,4,104000100,1 +10463,4,104000110,1 +10464,4,107000090,1 +10465,4,104000180,1 +10466,4,111000040,1 +10467,4,112000040,1 +10468,4,108000100,1 +10469,4,105000090,1 +10470,4,110000100,1 +10471,4,118000050,1 +10472,4,118000060,1 +10473,5,101000120,1 +10474,5,109000110,1 +10475,5,105000120,1 +10476,5,102000120,1 +10477,5,107000110,1 +10478,5,103000120,1 +10479,5,106000120,1 +10480,5,104000120,1 +10481,5,104000190,1 +10482,5,111000060,1 +10483,5,112000060,1 +10484,5,108000110,1 +10485,5,110000110,1 +10486,5,118000070,1 +10487,1,201000010,8 +10488,1,292000010,8 +10489,1,299000040,8 +10490,1,299000070,8 +10491,1,299000110,8 +10492,1,299000120,8 +10493,1,299000140,8 +10494,2,202000010,8 +10495,2,290000010,8 +10496,2,299000010,8 +10497,2,299000150,8 +10498,2,299000190,8 +10499,2,299000200,8 +10500,2,299000210,8 +10501,3,298000050,8 +10502,3,298000060,8 +10503,3,299000060,8 +10504,3,299000170,8 +10505,3,290000120,8 +10506,3,291000050,8 +10507,3,292000020,8 +10508,4,299000670,8 +10509,4,299000680,8 +10510,4,204000010,8 +10511,4,209000040,8 +10512,4,202000070,8 +10513,5,297000100,8 +10514,5,291000020,8 +10515,5,297000130,8 +10516,5,297000140,8 +10517,5,203000010,8 +10518,5,206000030,8 +10519,5,203000050,8 +10520,1,170002,3 +10521,1,180002,3 +10522,2,170003,3 +10523,2,180003,3 +10524,3,170004,3 +10525,3,180004,3 +10526,4,140000,3 +10527,5,150010,3 +10528,5,150020,3 +10529,5,150030,3 +10530,5,150040,3 +10532,1,101000010,1 +10533,1,102000010,1 +10534,1,103000010,1 +10535,1,104000010,1 +10536,1,105000010,1 +10537,1,106000010,1 +10538,1,107000010,1 +10539,1,108000010,1 +10540,1,109000010,1 +10541,1,110000010,1 +10542,2,101000020,1 +10543,2,101000030,1 +10544,2,102000020,1 +10545,2,102000030,1 +10546,2,102000040,1 +10547,2,103000020,1 +10548,2,103000030,1 +10549,2,103000040,1 +10550,2,104000020,1 +10551,2,104000030,1 +10552,2,104000040,1 +10553,2,105000020,1 +10554,2,105000030,1 +10555,2,105000040,1 +10556,2,106000020,1 +10557,2,106000030,1 +10558,2,106000040,1 +10559,2,107000020,1 +10560,2,107000030,1 +10561,2,107000040,1 +10562,2,108000020,1 +10563,2,108000030,1 +10564,2,108000040,1 +10565,2,109000020,1 +10566,2,109000030,1 +10567,2,109000040,1 +10568,2,110000020,1 +10569,2,110000030,1 +10570,2,110000040,1 +10571,2,118000010,1 +10572,3,101000050,1 +10573,3,101000060,1 +10574,3,101000080,1 +10575,3,101000040,1 +10576,3,109000060,1 +10577,3,109000070,1 +10578,3,109000080,1 +10579,3,105000060,1 +10580,3,105000070,1 +10581,3,105000080,1 +10582,3,104000050,1 +10583,3,106000050,1 +10584,3,102000060,1 +10585,3,102000070,1 +10586,3,102000080,1 +10587,3,103000050,1 +10588,3,105000050,1 +10589,3,107000060,1 +10590,3,107000070,1 +10591,3,107000080,1 +10592,3,108000050,1 +10593,3,109000050,1 +10594,3,103000060,1 +10595,3,103000070,1 +10596,3,103000080,1 +10597,3,110000050,1 +10598,3,106000060,1 +10599,3,106000070,1 +10600,3,106000080,1 +10601,3,101000070,1 +10602,3,110000060,1 +10603,3,104000060,1 +10604,3,104000070,1 +10605,3,104000080,1 +10606,3,102000050,1 +10607,3,104000170,1 +10608,3,104000260,1 +10609,3,111000010,1 +10610,3,111000020,1 +10611,3,111000030,1 +10612,3,112000020,1 +10613,3,112000030,1 +10614,3,108000060,1 +10615,3,108000070,1 +10616,3,108000080,1 +10617,3,107000050,1 +10618,3,112000010,1 +10619,3,110000070,1 +10620,3,110000080,1 +10621,3,118000020,1 +10622,3,118000030,1 +10623,3,118000040,1 +10624,4,101000090,1 +10625,4,101000100,1 +10626,4,101000110,1 +10627,4,109000100,1 +10628,4,105000100,1 +10629,4,105000110,1 +10630,4,108000090,1 +10631,4,110000090,1 +10632,4,102000100,1 +10633,4,102000110,1 +10634,4,106000090,1 +10635,4,109000090,1 +10636,4,107000100,1 +10637,4,103000090,1 +10638,4,102000090,1 +10639,4,103000100,1 +10640,4,106000100,1 +10641,4,106000110,1 +10642,4,104000090,1 +10643,4,104000100,1 +10644,4,104000110,1 +10645,4,107000090,1 +10646,4,104000180,1 +10647,4,111000040,1 +10648,4,112000040,1 +10649,4,108000100,1 +10650,4,105000090,1 +10651,4,110000100,1 +10652,4,118000050,1 +10653,4,118000060,1 +10654,5,101000120,1 +10655,5,109000110,1 +10656,5,105000120,1 +10657,5,102000120,1 +10658,5,107000110,1 +10659,5,103000120,1 +10660,5,106000120,1 +10661,5,104000120,1 +10662,5,104000190,1 +10663,5,111000060,1 +10664,5,112000060,1 +10665,5,108000110,1 +10666,5,110000110,1 +10667,5,118000070,1 +10668,1,201000010,8 +10669,1,292000010,8 +10670,1,299000040,8 +10671,1,299000070,8 +10672,1,299000110,8 +10673,1,299000120,8 +10674,1,299000140,8 +10675,2,202000010,8 +10676,2,290000010,8 +10677,2,299000010,8 +10678,2,299000150,8 +10679,2,299000190,8 +10680,2,299000200,8 +10681,2,299000210,8 +10682,3,298000050,8 +10683,3,298000060,8 +10684,3,299000060,8 +10685,3,299000170,8 +10686,3,290000120,8 +10687,3,291000050,8 +10688,3,292000020,8 +10689,4,299000670,8 +10690,4,299000680,8 +10691,4,204000010,8 +10692,4,209000040,8 +10693,4,202000070,8 +10694,5,297000100,8 +10695,5,291000020,8 +10696,5,297000130,8 +10697,5,297000140,8 +10698,5,203000010,8 +10699,5,206000030,8 +10700,5,203000050,8 +10701,2,170003,3 +10702,2,180003,3 +10703,3,170004,3 +10704,3,180004,3 +10705,4,140000,3 +10706,5,150010,3 +10707,5,150020,3 +10708,5,150030,3 +10709,5,150040,3 +10711,3,101000050,1 +10712,3,101000060,1 +10713,3,101000080,1 +10714,3,101000040,1 +10715,3,109000060,1 +10716,3,109000070,1 +10717,3,109000080,1 +10718,3,105000060,1 +10719,3,105000070,1 +10720,3,105000080,1 +10721,3,104000050,1 +10722,3,106000050,1 +10723,3,102000060,1 +10724,3,102000070,1 +10725,3,102000080,1 +10726,3,103000050,1 +10727,3,105000050,1 +10728,3,107000060,1 +10729,3,107000070,1 +10730,3,107000080,1 +10731,3,108000050,1 +10732,3,109000050,1 +10733,3,103000060,1 +10734,3,103000070,1 +10735,3,103000080,1 +10736,3,110000050,1 +10737,3,106000060,1 +10738,3,106000070,1 +10739,3,106000080,1 +10740,3,101000070,1 +10741,3,110000060,1 +10742,3,104000060,1 +10743,3,104000070,1 +10744,3,104000080,1 +10745,3,102000050,1 +10746,3,104000170,1 +10747,3,104000260,1 +10748,3,111000010,1 +10749,3,111000020,1 +10750,3,111000030,1 +10751,3,112000020,1 +10752,3,112000030,1 +10753,3,108000060,1 +10754,3,108000070,1 +10755,3,108000080,1 +10756,3,107000050,1 +10757,3,112000010,1 +10758,3,110000070,1 +10759,3,110000080,1 +10760,3,118000020,1 +10761,3,118000030,1 +10762,3,118000040,1 +10763,4,101000090,1 +10764,4,101000100,1 +10765,4,101000110,1 +10766,4,109000100,1 +10767,4,105000100,1 +10768,4,105000110,1 +10769,4,108000090,1 +10770,4,110000090,1 +10771,4,102000100,1 +10772,4,102000110,1 +10773,4,106000090,1 +10774,4,109000090,1 +10775,4,107000100,1 +10776,4,103000090,1 +10777,4,102000090,1 +10778,4,103000100,1 +10779,4,106000100,1 +10780,4,106000110,1 +10781,4,104000090,1 +10782,4,104000100,1 +10783,4,104000110,1 +10784,4,107000090,1 +10785,4,104000180,1 +10786,4,111000040,1 +10787,4,112000040,1 +10788,4,108000100,1 +10789,4,105000090,1 +10790,4,110000100,1 +10791,4,118000050,1 +10792,4,118000060,1 +10793,5,101000120,1 +10794,5,109000110,1 +10795,5,105000120,1 +10796,5,102000120,1 +10797,5,107000110,1 +10798,5,103000120,1 +10799,5,106000120,1 +10800,5,104000120,1 +10801,5,104000190,1 +10802,5,111000060,1 +10803,5,112000060,1 +10804,5,108000110,1 +10805,5,110000110,1 +10806,5,118000070,1 +10807,1,201000010,8 +10808,1,292000010,8 +10809,1,299000040,8 +10810,1,299000070,8 +10811,1,299000110,8 +10812,1,299000120,8 +10813,1,299000140,8 +10814,2,202000010,8 +10815,2,290000010,8 +10816,2,299000010,8 +10817,2,299000150,8 +10818,2,299000190,8 +10819,2,299000200,8 +10820,2,299000210,8 +10821,3,298000050,8 +10822,3,298000060,8 +10823,3,299000060,8 +10824,3,299000170,8 +10825,3,290000120,8 +10826,3,291000050,8 +10827,3,292000020,8 +10828,4,299000670,8 +10829,4,299000680,8 +10830,4,204000010,8 +10831,4,209000040,8 +10832,4,202000070,8 +10833,5,297000100,8 +10834,5,291000020,8 +10835,5,297000130,8 +10836,5,297000140,8 +10837,5,203000010,8 +10838,5,206000030,8 +10839,5,203000050,8 +10840,3,170004,3 +10841,3,180004,3 +10842,4,140000,3 +10843,5,150010,3 +10844,5,150020,3 +10845,5,150030,3 +10846,5,150040,3 +10848,3,101000050,1 +10849,3,101000060,1 +10850,3,101000080,1 +10851,3,101000040,1 +10852,3,109000060,1 +10853,3,109000070,1 +10854,3,109000080,1 +10855,3,105000060,1 +10856,3,105000070,1 +10857,3,105000080,1 +10858,3,104000050,1 +10859,3,106000050,1 +10860,3,102000060,1 +10861,3,102000070,1 +10862,3,102000080,1 +10863,3,103000050,1 +10864,3,105000050,1 +10865,3,107000060,1 +10866,3,107000070,1 +10867,3,107000080,1 +10868,3,108000050,1 +10869,3,109000050,1 +10870,3,103000060,1 +10871,3,103000070,1 +10872,3,103000080,1 +10873,3,110000050,1 +10874,3,106000060,1 +10875,3,106000070,1 +10876,3,106000080,1 +10877,3,101000070,1 +10878,3,110000060,1 +10879,3,104000060,1 +10880,3,104000070,1 +10881,3,104000080,1 +10882,3,102000050,1 +10883,3,104000170,1 +10884,3,104000260,1 +10885,3,111000010,1 +10886,3,111000020,1 +10887,3,111000030,1 +10888,3,112000020,1 +10889,3,112000030,1 +10890,3,108000060,1 +10891,3,108000070,1 +10892,3,108000080,1 +10893,3,107000050,1 +10894,3,112000010,1 +10895,3,110000070,1 +10896,3,110000080,1 +10897,3,118000020,1 +10898,3,118000030,1 +10899,3,118000040,1 +10900,4,101000090,1 +10901,4,101000100,1 +10902,4,101000110,1 +10903,4,109000100,1 +10904,4,105000100,1 +10905,4,105000110,1 +10906,4,108000090,1 +10907,4,110000090,1 +10908,4,102000100,1 +10909,4,102000110,1 +10910,4,106000090,1 +10911,4,109000090,1 +10912,4,107000100,1 +10913,4,103000090,1 +10914,4,102000090,1 +10915,4,103000100,1 +10916,4,106000100,1 +10917,4,106000110,1 +10918,4,104000090,1 +10919,4,104000100,1 +10920,4,104000110,1 +10921,4,107000090,1 +10922,4,104000180,1 +10923,4,111000040,1 +10924,4,112000040,1 +10925,4,108000100,1 +10926,4,105000090,1 +10927,4,110000100,1 +10928,4,118000050,1 +10929,4,118000060,1 +10930,5,101000120,1 +10931,5,109000110,1 +10932,5,105000120,1 +10933,5,102000120,1 +10934,5,107000110,1 +10935,5,103000120,1 +10936,5,106000120,1 +10937,5,104000120,1 +10938,5,104000190,1 +10939,5,111000060,1 +10940,5,112000060,1 +10941,5,108000110,1 +10942,5,110000110,1 +10943,5,118000070,1 +10944,1,201000010,8 +10945,1,292000010,8 +10946,1,299000040,8 +10947,1,299000070,8 +10948,1,299000110,8 +10949,1,299000120,8 +10950,1,299000140,8 +10951,2,202000010,8 +10952,2,290000010,8 +10953,2,299000010,8 +10954,2,299000150,8 +10955,2,299000190,8 +10956,2,299000200,8 +10957,2,299000210,8 +10958,3,298000050,8 +10959,3,298000060,8 +10960,3,299000060,8 +10961,3,299000170,8 +10962,3,290000120,8 +10963,3,291000050,8 +10964,3,292000020,8 +10965,4,299000670,8 +10966,4,299000680,8 +10967,4,204000010,8 +10968,4,209000040,8 +10969,4,202000070,8 +10970,5,297000100,8 +10971,5,291000020,8 +10972,5,297000130,8 +10973,5,297000140,8 +10974,5,203000010,8 +10975,5,206000030,8 +10976,5,203000050,8 +10977,3,170004,3 +10978,3,180004,3 +10979,4,140000,3 +10980,5,150010,3 +10981,5,150020,3 +10982,5,150030,3 +10983,5,150040,3 +10985,3,101000050,1 +10986,3,101000060,1 +10987,3,101000080,1 +10988,3,101000040,1 +10989,3,109000060,1 +10990,3,109000070,1 +10991,3,109000080,1 +10992,3,105000060,1 +10993,3,105000070,1 +10994,3,105000080,1 +10995,3,104000050,1 +10996,3,106000050,1 +10997,3,102000060,1 +10998,3,102000070,1 +10999,3,102000080,1 +11000,3,103000050,1 +11001,3,105000050,1 +11002,3,107000060,1 +11003,3,107000070,1 +11004,3,107000080,1 +11005,3,108000050,1 +11006,3,109000050,1 +11007,3,103000060,1 +11008,3,103000070,1 +11009,3,103000080,1 +11010,3,110000050,1 +11011,3,106000060,1 +11012,3,106000070,1 +11013,3,106000080,1 +11014,3,101000070,1 +11015,3,110000060,1 +11016,3,104000060,1 +11017,3,104000070,1 +11018,3,104000080,1 +11019,3,102000050,1 +11020,3,104000170,1 +11021,3,104000260,1 +11022,3,111000010,1 +11023,3,111000020,1 +11024,3,111000030,1 +11025,3,112000020,1 +11026,3,112000030,1 +11027,3,108000060,1 +11028,3,108000070,1 +11029,3,108000080,1 +11030,3,107000050,1 +11031,3,112000010,1 +11032,3,110000070,1 +11033,3,110000080,1 +11034,3,118000020,1 +11035,3,118000030,1 +11036,3,118000040,1 +11037,4,101000090,1 +11038,4,101000100,1 +11039,4,101000110,1 +11040,4,109000100,1 +11041,4,105000100,1 +11042,4,105000110,1 +11043,4,108000090,1 +11044,4,110000090,1 +11045,4,102000100,1 +11046,4,102000110,1 +11047,4,106000090,1 +11048,4,109000090,1 +11049,4,107000100,1 +11050,4,103000090,1 +11051,4,102000090,1 +11052,4,103000100,1 +11053,4,106000100,1 +11054,4,106000110,1 +11055,4,104000090,1 +11056,4,104000100,1 +11057,4,104000110,1 +11058,4,107000090,1 +11059,4,104000180,1 +11060,4,111000040,1 +11061,4,112000040,1 +11062,4,108000100,1 +11063,4,105000090,1 +11064,4,110000100,1 +11065,4,118000050,1 +11066,4,118000060,1 +11067,5,101000120,1 +11068,5,109000110,1 +11069,5,105000120,1 +11070,5,102000120,1 +11071,5,107000110,1 +11072,5,103000120,1 +11073,5,106000120,1 +11074,5,104000120,1 +11075,5,104000190,1 +11076,5,111000060,1 +11077,5,112000060,1 +11078,5,108000110,1 +11079,5,110000110,1 +11080,5,118000070,1 +11081,1,201000010,8 +11082,1,292000010,8 +11083,1,299000040,8 +11084,1,299000070,8 +11085,1,299000110,8 +11086,1,299000120,8 +11087,1,299000140,8 +11088,2,202000010,8 +11089,2,290000010,8 +11090,2,299000010,8 +11091,2,299000150,8 +11092,2,299000190,8 +11093,2,299000200,8 +11094,2,299000210,8 +11095,3,298000050,8 +11096,3,298000060,8 +11097,3,299000060,8 +11098,3,299000170,8 +11099,3,290000120,8 +11100,3,291000050,8 +11101,3,292000020,8 +11102,4,299000670,8 +11103,4,299000680,8 +11104,4,204000010,8 +11105,4,209000040,8 +11106,4,202000070,8 +11107,5,297000100,8 +11108,5,291000020,8 +11109,5,297000130,8 +11110,5,297000140,8 +11111,5,203000010,8 +11112,5,206000030,8 +11113,5,203000050,8 +11114,3,170004,3 +11115,3,180004,3 +11116,4,140000,3 +11117,5,150010,3 +11118,5,150020,3 +11119,5,150030,3 +11120,5,150040,3 +11121,5,190000,3 +11122,5,200000,3 +11123,5,210000,3 +11125,3,118000020,1 +11126,3,118000030,1 +11127,3,118000040,1 +11128,3,106000060,1 +11129,3,106000070,1 +11130,3,106000080,1 +11131,3,101000070,1 +11132,3,110000060,1 +11133,3,104000060,1 +11134,3,104000070,1 +11135,3,104000080,1 +11136,3,102000050,1 +11137,3,104000170,1 +11138,3,104000260,1 +11139,4,118000050,1 +11140,4,118000060,1 +11141,4,106000100,1 +11142,4,106000110,1 +11143,4,104000090,1 +11144,4,104000100,1 +11145,4,104000110,1 +11146,4,104000270,1 +11147,4,107000090,1 +11148,4,104000180,1 +11149,5,118000070,1 +11150,5,106000120,1 +11151,5,104000120,1 +11152,5,104000190,1 +11153,1,103000000,2 +11154,2,103000001,2 +11155,3,103000002,2 +11156,4,103000003,2 +11157,5,103000004,2 +11158,1,111000000,2 +11159,2,111000001,2 +11160,3,111000002,2 +11161,4,111000003,2 +11162,5,111000004,2 +11163,1,115000000,2 +11164,2,115000001,2 +11165,3,115000002,2 +11166,4,115000003,2 +11167,5,115000004,2 +11168,3,170004,3 +11169,4,170005,3 +11170,3,180004,3 +11171,4,180005,3 +11172,4,140000,3 +11173,4,150010,3 +11174,4,150020,3 +11175,4,150030,3 +11176,4,150040,3 +11178,3,111000010,1 +11179,3,111000020,1 +11180,3,111000030,1 +11181,3,112000020,1 +11182,3,112000030,1 +11183,3,108000060,1 +11184,3,108000070,1 +11185,3,108000080,1 +11186,3,107000050,1 +11187,3,112000010,1 +11188,3,110000070,1 +11189,3,110000080,1 +11190,4,111000040,1 +11191,4,112000040,1 +11192,4,108000100,1 +11193,4,105000090,1 +11194,4,110000100,1 +11195,5,111000060,1 +11196,5,112000060,1 +11197,5,108000110,1 +11198,5,110000110,1 +11199,1,108000000,2 +11200,2,108000001,2 +11201,3,108000002,2 +11202,4,108000003,2 +11203,5,108000004,2 +11204,1,107000000,2 +11205,2,107000001,2 +11206,3,107000002,2 +11207,4,107000003,2 +11208,5,107000004,2 +11209,1,120000000,2 +11210,2,120000001,2 +11211,3,120000002,2 +11212,4,120000003,2 +11213,5,120000004,2 +11214,4,110024,3 +11215,4,110034,3 +11216,4,110044,3 +11217,4,110054,3 +11218,3,110060,3 +11219,3,110070,3 +11220,3,110080,3 +11221,3,110090,3 +11222,3,110100,3 +11223,3,110110,3 +11224,3,110120,3 +11225,3,110130,3 +11226,3,110140,3 +11227,3,110150,3 +11228,3,110160,3 +11229,3,110170,3 +11230,3,110180,3 +11231,3,110190,3 +11232,3,110200,3 +11233,3,110210,3 +11234,3,110220,3 +11235,3,110230,3 +11236,3,110240,3 +11237,3,110250,3 +11238,3,110260,3 +11239,3,110270,3 +11240,3,110620,3 +11241,3,110670,3 +11242,4,140000,3 +11243,4,150010,3 +11244,4,150020,3 +11245,4,150030,3 +11246,4,150040,3 +11248,3,101000050,1 +11249,3,101000060,1 +11250,3,101000080,1 +11251,3,101000040,1 +11252,3,109000060,1 +11253,3,109000070,1 +11254,3,109000080,1 +11255,3,105000060,1 +11256,3,105000070,1 +11257,3,105000080,1 +11258,3,104000050,1 +11259,3,106000050,1 +11260,4,101000090,1 +11261,4,101000100,1 +11262,4,101000110,1 +11263,4,109000100,1 +11264,4,105000100,1 +11265,4,105000110,1 +11266,4,108000090,1 +11267,4,110000090,1 +11268,5,101000120,1 +11269,5,109000110,1 +11270,5,105000120,1 +11271,1,101000000,2 +11272,2,101000001,2 +11273,3,101000002,2 +11274,4,101000008,2 +11275,5,101000004,2 +11276,1,109000000,2 +11277,2,109000001,2 +11278,3,109000002,2 +11279,4,109000003,2 +11280,5,109000004,2 +11281,4,120024,3 +11282,4,120034,3 +11283,4,120044,3 +11284,4,120054,3 +11285,3,120241,3 +11286,3,120251,3 +11287,3,120261,3 +11288,3,120271,3 +11289,3,120300,3 +11290,3,120310,3 +11291,3,120320,3 +11292,3,120330,3 +11293,3,120340,3 +11294,3,120350,3 +11295,3,120360,3 +11296,3,120370,3 +11297,3,120380,3 +11298,3,120390,3 +11299,3,120400,3 +11300,3,120410,3 +11301,3,120420,3 +11302,3,120430,3 +11303,3,120450,3 +11304,3,120460,3 +11305,3,120550,3 +11306,3,120560,3 +11307,3,120570,3 +11308,3,120990,3 +11309,3,121000,3 +11310,3,121010,3 +11311,3,121020,3 +11312,3,121100,3 +11313,4,140000,3 +11314,4,150010,3 +11315,4,150020,3 +11316,4,150030,3 +11317,4,150040,3 +11319,3,102000060,1 +11320,3,102000070,1 +11321,3,102000080,1 +11322,3,103000050,1 +11323,3,105000050,1 +11324,3,107000060,1 +11325,3,107000070,1 +11326,3,107000080,1 +11327,3,108000050,1 +11328,3,109000050,1 +11329,3,103000060,1 +11330,3,103000070,1 +11331,3,103000080,1 +11332,3,110000050,1 +11333,4,102000100,1 +11334,4,102000110,1 +11335,4,102000350,1 +11336,4,106000090,1 +11337,4,109000090,1 +11338,4,107000100,1 +11339,4,103000090,1 +11340,4,102000090,1 +11341,4,103000100,1 +11342,5,102000120,1 +11343,5,107000110,1 +11344,5,103000120,1 +11345,1,102000000,2 +11346,2,102000001,2 +11347,3,102000002,2 +11348,4,102000003,2 +11349,5,102000004,2 +11350,1,105000000,2 +11351,2,105000001,2 +11352,3,105000002,2 +11353,4,105000003,2 +11354,5,105000004,2 +11355,1,112000000,2 +11356,2,112000001,2 +11357,3,112000002,2 +11358,4,112000003,2 +11359,5,112000004,2 +11360,4,130024,3 +11361,4,130034,3 +11362,4,130044,3 +11363,4,130054,3 +11364,3,130060,3 +11365,3,130070,3 +11366,3,130080,3 +11367,3,130090,3 +11368,3,130100,3 +11369,3,130110,3 +11370,3,130120,3 +11371,3,130130,3 +11372,3,130140,3 +11373,3,130150,3 +11374,3,130160,3 +11375,3,130170,3 +11376,3,130180,3 +11377,3,130190,3 +11378,3,130200,3 +11379,3,130420,3 +11380,3,130510,3 +11381,3,130520,3 +11382,3,130531,3 +11383,3,130540,3 +11384,3,130660,3 +11385,3,130790,3 +11386,3,130800,3 +11387,3,131130,3 +11388,4,140000,3 +11389,4,150010,3 +11390,4,150020,3 +11391,4,150030,3 +11392,4,150040,3 +11394,1,101000010,1 +11395,1,102000010,1 +11396,1,103000010,1 +11397,1,104000010,1 +11398,1,105000010,1 +11399,1,106000010,1 +11400,1,107000010,1 +11401,1,108000010,1 +11402,1,109000010,1 +11403,1,110000010,1 +11404,2,101000020,1 +11405,2,101000030,1 +11406,2,102000020,1 +11407,2,102000030,1 +11408,2,102000040,1 +11409,2,103000020,1 +11410,2,103000030,1 +11411,2,103000040,1 +11412,2,104000020,1 +11413,2,104000030,1 +11414,2,104000040,1 +11415,2,105000020,1 +11416,2,105000030,1 +11417,2,105000040,1 +11418,2,106000020,1 +11419,2,106000030,1 +11420,2,106000040,1 +11421,2,107000020,1 +11422,2,107000030,1 +11423,2,107000040,1 +11424,2,108000020,1 +11425,2,108000030,1 +11426,2,108000040,1 +11427,2,109000020,1 +11428,2,109000030,1 +11429,2,109000040,1 +11430,2,110000020,1 +11431,2,110000030,1 +11432,2,110000040,1 +11433,2,118000010,1 +11434,3,101000050,1 +11435,3,101000060,1 +11436,3,101000080,1 +11437,3,101000040,1 +11438,3,109000060,1 +11439,3,109000070,1 +11440,3,109000080,1 +11441,3,105000060,1 +11442,3,105000070,1 +11443,3,105000080,1 +11444,3,104000050,1 +11445,3,106000050,1 +11446,3,102000060,1 +11447,3,102000070,1 +11448,3,102000080,1 +11449,3,103000050,1 +11450,3,105000050,1 +11451,3,107000060,1 +11452,3,107000070,1 +11453,3,107000080,1 +11454,3,108000050,1 +11455,3,109000050,1 +11456,3,103000060,1 +11457,3,103000070,1 +11458,3,103000080,1 +11459,3,110000050,1 +11460,3,106000060,1 +11461,3,106000070,1 +11462,3,106000080,1 +11463,3,101000070,1 +11464,3,110000060,1 +11465,3,104000060,1 +11466,3,104000070,1 +11467,3,104000080,1 +11468,3,102000050,1 +11469,3,104000170,1 +11470,3,104000260,1 +11471,3,111000010,1 +11472,3,111000020,1 +11473,3,111000030,1 +11474,3,112000020,1 +11475,3,112000030,1 +11476,3,108000060,1 +11477,3,108000070,1 +11478,3,108000080,1 +11479,3,107000050,1 +11480,3,112000010,1 +11481,3,110000070,1 +11482,3,110000080,1 +11483,3,118000020,1 +11484,3,118000030,1 +11485,3,118000040,1 +11486,4,101000090,1 +11487,4,101000100,1 +11488,4,101000110,1 +11489,4,109000100,1 +11490,4,105000100,1 +11491,4,105000110,1 +11492,4,108000090,1 +11493,4,110000090,1 +11494,4,102000100,1 +11495,4,102000110,1 +11496,4,106000090,1 +11497,4,109000090,1 +11498,4,107000100,1 +11499,4,103000090,1 +11500,4,102000090,1 +11501,4,103000100,1 +11502,4,106000100,1 +11503,4,106000110,1 +11504,4,104000090,1 +11505,4,104000100,1 +11506,4,104000110,1 +11507,4,107000090,1 +11508,4,104000180,1 +11509,4,111000040,1 +11510,4,112000040,1 +11511,4,108000100,1 +11512,4,105000090,1 +11513,4,110000100,1 +11514,4,118000050,1 +11515,4,118000060,1 +11516,5,101000120,1 +11517,5,109000110,1 +11518,5,105000120,1 +11519,5,102000120,1 +11520,5,107000110,1 +11521,5,103000120,1 +11522,5,106000120,1 +11523,5,104000120,1 +11524,5,104000190,1 +11525,5,111000060,1 +11526,5,112000060,1 +11527,5,108000110,1 +11528,5,110000110,1 +11529,5,118000070,1 +11530,1,201000010,8 +11531,1,292000010,8 +11532,1,299000040,8 +11533,1,299000070,8 +11534,1,299000110,8 +11535,1,299000120,8 +11536,1,299000140,8 +11537,2,202000010,8 +11538,2,290000010,8 +11539,2,299000010,8 +11540,2,299000150,8 +11541,2,299000190,8 +11542,2,299000200,8 +11543,2,299000210,8 +11544,3,298000050,8 +11545,3,298000060,8 +11546,3,299000060,8 +11547,3,299000170,8 +11548,3,290000120,8 +11549,3,291000050,8 +11550,3,292000020,8 +11551,4,299000670,8 +11552,4,299000680,8 +11553,4,204000010,8 +11554,4,209000040,8 +11555,4,202000070,8 +11556,4,209000070,8 +11557,5,297000100,8 +11558,5,291000020,8 +11559,5,297000130,8 +11560,5,297000140,8 +11561,5,203000010,8 +11562,5,206000030,8 +11563,5,203000050,8 +11564,5,202000090,8 +11565,1,170002,3 +11566,1,180002,3 +11567,2,170003,3 +11568,2,180003,3 +11569,3,170004,3 +11570,3,180004,3 +11571,4,140000,3 +11572,5,150010,3 +11573,5,150020,3 +11574,5,150030,3 +11575,5,150040,3 +11577,1,101000010,1 +11578,1,102000010,1 +11579,1,103000010,1 +11580,1,104000010,1 +11581,1,105000010,1 +11582,1,106000010,1 +11583,1,107000010,1 +11584,1,108000010,1 +11585,1,109000010,1 +11586,1,110000010,1 +11587,2,101000020,1 +11588,2,101000030,1 +11589,2,102000020,1 +11590,2,102000030,1 +11591,2,102000040,1 +11592,2,103000020,1 +11593,2,103000030,1 +11594,2,103000040,1 +11595,2,104000020,1 +11596,2,104000030,1 +11597,2,104000040,1 +11598,2,105000020,1 +11599,2,105000030,1 +11600,2,105000040,1 +11601,2,106000020,1 +11602,2,106000030,1 +11603,2,106000040,1 +11604,2,107000020,1 +11605,2,107000030,1 +11606,2,107000040,1 +11607,2,108000020,1 +11608,2,108000030,1 +11609,2,108000040,1 +11610,2,109000020,1 +11611,2,109000030,1 +11612,2,109000040,1 +11613,2,110000020,1 +11614,2,110000030,1 +11615,2,110000040,1 +11616,2,118000010,1 +11617,3,101000050,1 +11618,3,101000060,1 +11619,3,101000080,1 +11620,3,101000040,1 +11621,3,109000060,1 +11622,3,109000070,1 +11623,3,109000080,1 +11624,3,105000060,1 +11625,3,105000070,1 +11626,3,105000080,1 +11627,3,104000050,1 +11628,3,106000050,1 +11629,3,102000060,1 +11630,3,102000070,1 +11631,3,102000080,1 +11632,3,103000050,1 +11633,3,105000050,1 +11634,3,107000060,1 +11635,3,107000070,1 +11636,3,107000080,1 +11637,3,108000050,1 +11638,3,109000050,1 +11639,3,103000060,1 +11640,3,103000070,1 +11641,3,103000080,1 +11642,3,110000050,1 +11643,3,106000060,1 +11644,3,106000070,1 +11645,3,106000080,1 +11646,3,101000070,1 +11647,3,110000060,1 +11648,3,104000060,1 +11649,3,104000070,1 +11650,3,104000080,1 +11651,3,102000050,1 +11652,3,104000170,1 +11653,3,104000260,1 +11654,3,111000010,1 +11655,3,111000020,1 +11656,3,111000030,1 +11657,3,112000020,1 +11658,3,112000030,1 +11659,3,108000060,1 +11660,3,108000070,1 +11661,3,108000080,1 +11662,3,107000050,1 +11663,3,112000010,1 +11664,3,110000070,1 +11665,3,110000080,1 +11666,3,118000020,1 +11667,3,118000030,1 +11668,3,118000040,1 +11669,4,101000090,1 +11670,4,101000100,1 +11671,4,101000110,1 +11672,4,109000100,1 +11673,4,105000100,1 +11674,4,105000110,1 +11675,4,108000090,1 +11676,4,110000090,1 +11677,4,102000100,1 +11678,4,102000110,1 +11679,4,106000090,1 +11680,4,109000090,1 +11681,4,107000100,1 +11682,4,103000090,1 +11683,4,102000090,1 +11684,4,103000100,1 +11685,4,106000100,1 +11686,4,106000110,1 +11687,4,104000090,1 +11688,4,104000100,1 +11689,4,104000110,1 +11690,4,107000090,1 +11691,4,104000180,1 +11692,4,111000040,1 +11693,4,112000040,1 +11694,4,108000100,1 +11695,4,105000090,1 +11696,4,110000100,1 +11697,4,118000050,1 +11698,4,118000060,1 +11699,5,101000120,1 +11700,5,109000110,1 +11701,5,105000120,1 +11702,5,102000120,1 +11703,5,107000110,1 +11704,5,103000120,1 +11705,5,106000120,1 +11706,5,104000120,1 +11707,5,104000190,1 +11708,5,111000060,1 +11709,5,112000060,1 +11710,5,108000110,1 +11711,5,110000110,1 +11712,5,118000070,1 +11713,1,201000010,8 +11714,1,292000010,8 +11715,1,299000040,8 +11716,1,299000070,8 +11717,1,299000110,8 +11718,1,299000120,8 +11719,1,299000140,8 +11720,2,202000010,8 +11721,2,290000010,8 +11722,2,299000010,8 +11723,2,299000150,8 +11724,2,299000190,8 +11725,2,299000200,8 +11726,2,299000210,8 +11727,3,298000050,8 +11728,3,298000060,8 +11729,3,299000060,8 +11730,3,299000170,8 +11731,3,290000120,8 +11732,3,291000050,8 +11733,3,292000020,8 +11734,4,299000670,8 +11735,4,299000680,8 +11736,4,204000010,8 +11737,4,209000040,8 +11738,4,202000070,8 +11739,4,209000070,8 +11740,5,297000100,8 +11741,5,291000020,8 +11742,5,297000130,8 +11743,5,297000140,8 +11744,5,203000010,8 +11745,5,206000030,8 +11746,5,203000050,8 +11747,5,202000090,8 +11748,2,170003,3 +11749,2,180003,3 +11750,3,170004,3 +11751,3,180004,3 +11752,4,140000,3 +11753,5,150010,3 +11754,5,150020,3 +11755,5,150030,3 +11756,5,150040,3 +11758,3,101000050,1 +11759,3,101000060,1 +11760,3,101000080,1 +11761,3,101000040,1 +11762,3,109000060,1 +11763,3,109000070,1 +11764,3,109000080,1 +11765,3,105000060,1 +11766,3,105000070,1 +11767,3,105000080,1 +11768,3,104000050,1 +11769,3,106000050,1 +11770,3,102000060,1 +11771,3,102000070,1 +11772,3,102000080,1 +11773,3,103000050,1 +11774,3,105000050,1 +11775,3,107000060,1 +11776,3,107000070,1 +11777,3,107000080,1 +11778,3,108000050,1 +11779,3,109000050,1 +11780,3,103000060,1 +11781,3,103000070,1 +11782,3,103000080,1 +11783,3,110000050,1 +11784,3,106000060,1 +11785,3,106000070,1 +11786,3,106000080,1 +11787,3,101000070,1 +11788,3,110000060,1 +11789,3,104000060,1 +11790,3,104000070,1 +11791,3,104000080,1 +11792,3,102000050,1 +11793,3,104000170,1 +11794,3,104000260,1 +11795,3,111000010,1 +11796,3,111000020,1 +11797,3,111000030,1 +11798,3,112000020,1 +11799,3,112000030,1 +11800,3,108000060,1 +11801,3,108000070,1 +11802,3,108000080,1 +11803,3,107000050,1 +11804,3,112000010,1 +11805,3,110000070,1 +11806,3,110000080,1 +11807,3,118000020,1 +11808,3,118000030,1 +11809,3,118000040,1 +11810,4,101000090,1 +11811,4,101000100,1 +11812,4,101000110,1 +11813,4,109000100,1 +11814,4,105000100,1 +11815,4,105000110,1 +11816,4,108000090,1 +11817,4,110000090,1 +11818,4,102000100,1 +11819,4,102000110,1 +11820,4,106000090,1 +11821,4,109000090,1 +11822,4,107000100,1 +11823,4,103000090,1 +11824,4,102000090,1 +11825,4,103000100,1 +11826,4,106000100,1 +11827,4,106000110,1 +11828,4,104000090,1 +11829,4,104000100,1 +11830,4,104000110,1 +11831,4,107000090,1 +11832,4,104000180,1 +11833,4,111000040,1 +11834,4,112000040,1 +11835,4,108000100,1 +11836,4,105000090,1 +11837,4,110000100,1 +11838,4,118000050,1 +11839,4,118000060,1 +11840,5,101000120,1 +11841,5,109000110,1 +11842,5,105000120,1 +11843,5,102000120,1 +11844,5,107000110,1 +11845,5,103000120,1 +11846,5,106000120,1 +11847,5,104000120,1 +11848,5,104000190,1 +11849,5,111000060,1 +11850,5,112000060,1 +11851,5,108000110,1 +11852,5,110000110,1 +11853,5,118000070,1 +11854,1,201000010,8 +11855,1,292000010,8 +11856,1,299000040,8 +11857,1,299000070,8 +11858,1,299000110,8 +11859,1,299000120,8 +11860,1,299000140,8 +11861,2,202000010,8 +11862,2,290000010,8 +11863,2,299000010,8 +11864,2,299000150,8 +11865,2,299000190,8 +11866,2,299000200,8 +11867,2,299000210,8 +11868,3,298000050,8 +11869,3,298000060,8 +11870,3,299000060,8 +11871,3,299000170,8 +11872,3,290000120,8 +11873,3,291000050,8 +11874,3,292000020,8 +11875,4,299000670,8 +11876,4,299000680,8 +11877,4,204000010,8 +11878,4,209000040,8 +11879,4,202000070,8 +11880,4,209000070,8 +11881,5,297000100,8 +11882,5,291000020,8 +11883,5,297000130,8 +11884,5,297000140,8 +11885,5,203000010,8 +11886,5,206000030,8 +11887,5,203000050,8 +11888,5,202000090,8 +11889,3,170004,3 +11890,3,180004,3 +11891,4,140000,3 +11892,5,150010,3 +11893,5,150020,3 +11894,5,150030,3 +11895,5,150040,3 +11897,3,101000050,1 +11898,3,101000060,1 +11899,3,101000080,1 +11900,3,101000040,1 +11901,3,109000060,1 +11902,3,109000070,1 +11903,3,109000080,1 +11904,3,105000060,1 +11905,3,105000070,1 +11906,3,105000080,1 +11907,3,104000050,1 +11908,3,106000050,1 +11909,3,102000060,1 +11910,3,102000070,1 +11911,3,102000080,1 +11912,3,103000050,1 +11913,3,105000050,1 +11914,3,107000060,1 +11915,3,107000070,1 +11916,3,107000080,1 +11917,3,108000050,1 +11918,3,109000050,1 +11919,3,103000060,1 +11920,3,103000070,1 +11921,3,103000080,1 +11922,3,110000050,1 +11923,3,106000060,1 +11924,3,106000070,1 +11925,3,106000080,1 +11926,3,101000070,1 +11927,3,110000060,1 +11928,3,104000060,1 +11929,3,104000070,1 +11930,3,104000080,1 +11931,3,102000050,1 +11932,3,104000170,1 +11933,3,104000260,1 +11934,3,111000010,1 +11935,3,111000020,1 +11936,3,111000030,1 +11937,3,112000020,1 +11938,3,112000030,1 +11939,3,108000060,1 +11940,3,108000070,1 +11941,3,108000080,1 +11942,3,107000050,1 +11943,3,112000010,1 +11944,3,110000070,1 +11945,3,110000080,1 +11946,3,118000020,1 +11947,3,118000030,1 +11948,3,118000040,1 +11949,4,101000090,1 +11950,4,101000100,1 +11951,4,101000110,1 +11952,4,109000100,1 +11953,4,105000100,1 +11954,4,105000110,1 +11955,4,108000090,1 +11956,4,110000090,1 +11957,4,102000100,1 +11958,4,102000110,1 +11959,4,106000090,1 +11960,4,109000090,1 +11961,4,107000100,1 +11962,4,103000090,1 +11963,4,102000090,1 +11964,4,103000100,1 +11965,4,106000100,1 +11966,4,106000110,1 +11967,4,104000090,1 +11968,4,104000100,1 +11969,4,104000110,1 +11970,4,107000090,1 +11971,4,104000180,1 +11972,4,111000040,1 +11973,4,112000040,1 +11974,4,108000100,1 +11975,4,105000090,1 +11976,4,110000100,1 +11977,4,118000050,1 +11978,4,118000060,1 +11979,5,101000120,1 +11980,5,109000110,1 +11981,5,105000120,1 +11982,5,102000120,1 +11983,5,107000110,1 +11984,5,103000120,1 +11985,5,106000120,1 +11986,5,104000120,1 +11987,5,104000190,1 +11988,5,111000060,1 +11989,5,112000060,1 +11990,5,108000110,1 +11991,5,110000110,1 +11992,5,118000070,1 +11993,1,201000010,8 +11994,1,292000010,8 +11995,1,299000040,8 +11996,1,299000070,8 +11997,1,299000110,8 +11998,1,299000120,8 +11999,1,299000140,8 +12000,2,202000010,8 +12001,2,290000010,8 +12002,2,299000010,8 +12003,2,299000150,8 +12004,2,299000190,8 +12005,2,299000200,8 +12006,2,299000210,8 +12007,3,298000050,8 +12008,3,298000060,8 +12009,3,299000060,8 +12010,3,299000170,8 +12011,3,290000120,8 +12012,3,291000050,8 +12013,3,292000020,8 +12014,4,299000670,8 +12015,4,299000680,8 +12016,4,204000010,8 +12017,4,209000040,8 +12018,4,202000070,8 +12019,4,209000070,8 +12020,5,297000100,8 +12021,5,291000020,8 +12022,5,297000130,8 +12023,5,297000140,8 +12024,5,203000010,8 +12025,5,206000030,8 +12026,5,203000050,8 +12027,5,202000090,8 +12028,3,170004,3 +12029,3,180004,3 +12030,4,140000,3 +12031,5,150010,3 +12032,5,150020,3 +12033,5,150030,3 +12034,5,150040,3 +12036,3,101000050,1 +12037,3,101000060,1 +12038,3,101000080,1 +12039,3,101000040,1 +12040,3,109000060,1 +12041,3,109000070,1 +12042,3,109000080,1 +12043,3,105000060,1 +12044,3,105000070,1 +12045,3,105000080,1 +12046,3,104000050,1 +12047,3,106000050,1 +12048,3,102000060,1 +12049,3,102000070,1 +12050,3,102000080,1 +12051,3,103000050,1 +12052,3,105000050,1 +12053,3,107000060,1 +12054,3,107000070,1 +12055,3,107000080,1 +12056,3,108000050,1 +12057,3,109000050,1 +12058,3,103000060,1 +12059,3,103000070,1 +12060,3,103000080,1 +12061,3,110000050,1 +12062,3,106000060,1 +12063,3,106000070,1 +12064,3,106000080,1 +12065,3,101000070,1 +12066,3,110000060,1 +12067,3,104000060,1 +12068,3,104000070,1 +12069,3,104000080,1 +12070,3,102000050,1 +12071,3,104000170,1 +12072,3,104000260,1 +12073,3,111000010,1 +12074,3,111000020,1 +12075,3,111000030,1 +12076,3,112000020,1 +12077,3,112000030,1 +12078,3,108000060,1 +12079,3,108000070,1 +12080,3,108000080,1 +12081,3,107000050,1 +12082,3,112000010,1 +12083,3,110000070,1 +12084,3,110000080,1 +12085,3,118000020,1 +12086,3,118000030,1 +12087,3,118000040,1 +12088,4,101000090,1 +12089,4,101000100,1 +12090,4,101000110,1 +12091,4,109000100,1 +12092,4,105000100,1 +12093,4,105000110,1 +12094,4,108000090,1 +12095,4,110000090,1 +12096,4,102000100,1 +12097,4,102000110,1 +12098,4,106000090,1 +12099,4,109000090,1 +12100,4,107000100,1 +12101,4,103000090,1 +12102,4,102000090,1 +12103,4,103000100,1 +12104,4,106000100,1 +12105,4,106000110,1 +12106,4,104000090,1 +12107,4,104000100,1 +12108,4,104000110,1 +12109,4,107000090,1 +12110,4,104000180,1 +12111,4,111000040,1 +12112,4,112000040,1 +12113,4,108000100,1 +12114,4,105000090,1 +12115,4,110000100,1 +12116,4,118000050,1 +12117,4,118000060,1 +12118,5,101000120,1 +12119,5,109000110,1 +12120,5,105000120,1 +12121,5,102000120,1 +12122,5,107000110,1 +12123,5,103000120,1 +12124,5,106000120,1 +12125,5,104000120,1 +12126,5,104000190,1 +12127,5,111000060,1 +12128,5,112000060,1 +12129,5,108000110,1 +12130,5,110000110,1 +12131,5,118000070,1 +12132,1,201000010,8 +12133,1,292000010,8 +12134,1,299000040,8 +12135,1,299000070,8 +12136,1,299000110,8 +12137,1,299000120,8 +12138,1,299000140,8 +12139,2,202000010,8 +12140,2,290000010,8 +12141,2,299000010,8 +12142,2,299000150,8 +12143,2,299000190,8 +12144,2,299000200,8 +12145,2,299000210,8 +12146,3,298000050,8 +12147,3,298000060,8 +12148,3,299000060,8 +12149,3,299000170,8 +12150,3,290000120,8 +12151,3,291000050,8 +12152,3,292000020,8 +12153,4,299000670,8 +12154,4,299000680,8 +12155,4,204000010,8 +12156,4,209000040,8 +12157,4,202000070,8 +12158,4,209000070,8 +12159,5,297000100,8 +12160,5,291000020,8 +12161,5,297000130,8 +12162,5,297000140,8 +12163,5,203000010,8 +12164,5,206000030,8 +12165,5,203000050,8 +12166,5,202000090,8 +12167,3,170004,3 +12168,3,180004,3 +12169,4,140000,3 +12170,5,150010,3 +12171,5,150020,3 +12172,5,150030,3 +12173,5,150040,3 +12174,5,190000,3 +12175,5,200000,3 +12176,5,210000,3 +12178,1,101000010,1 +12179,1,102000010,1 +12180,1,103000010,1 +12181,1,104000010,1 +12182,1,105000010,1 +12183,1,106000010,1 +12184,1,107000010,1 +12185,1,108000010,1 +12186,1,109000010,1 +12187,1,110000010,1 +12188,2,101000020,1 +12189,2,101000030,1 +12190,2,102000020,1 +12191,2,102000030,1 +12192,2,102000040,1 +12193,2,103000020,1 +12194,2,103000030,1 +12195,2,103000040,1 +12196,2,104000020,1 +12197,2,104000030,1 +12198,2,104000040,1 +12199,2,105000020,1 +12200,2,105000030,1 +12201,2,105000040,1 +12202,2,106000020,1 +12203,2,106000030,1 +12204,2,106000040,1 +12205,2,107000020,1 +12206,2,107000030,1 +12207,2,107000040,1 +12208,2,108000020,1 +12209,2,108000030,1 +12210,2,108000040,1 +12211,2,109000020,1 +12212,2,109000030,1 +12213,2,109000040,1 +12214,2,110000020,1 +12215,2,110000030,1 +12216,2,110000040,1 +12217,2,118000010,1 +12218,3,101000050,1 +12219,3,101000060,1 +12220,3,101000080,1 +12221,3,101000040,1 +12222,3,109000060,1 +12223,3,109000070,1 +12224,3,109000080,1 +12225,3,105000060,1 +12226,3,105000070,1 +12227,3,105000080,1 +12228,3,104000050,1 +12229,3,106000050,1 +12230,3,102000060,1 +12231,3,102000070,1 +12232,3,102000080,1 +12233,3,103000050,1 +12234,3,105000050,1 +12235,3,107000060,1 +12236,3,107000070,1 +12237,3,107000080,1 +12238,3,108000050,1 +12239,3,109000050,1 +12240,3,103000060,1 +12241,3,103000070,1 +12242,3,103000080,1 +12243,3,110000050,1 +12244,3,106000060,1 +12245,3,106000070,1 +12246,3,106000080,1 +12247,3,101000070,1 +12248,3,110000060,1 +12249,3,104000060,1 +12250,3,104000070,1 +12251,3,104000080,1 +12252,3,102000050,1 +12253,3,104000170,1 +12254,3,104000260,1 +12255,3,111000010,1 +12256,3,111000020,1 +12257,3,111000030,1 +12258,3,112000020,1 +12259,3,112000030,1 +12260,3,108000060,1 +12261,3,108000070,1 +12262,3,108000080,1 +12263,3,107000050,1 +12264,3,112000010,1 +12265,3,110000070,1 +12266,3,110000080,1 +12267,3,118000020,1 +12268,3,118000030,1 +12269,3,118000040,1 +12270,4,101000090,1 +12271,4,101000100,1 +12272,4,101000110,1 +12273,4,109000100,1 +12274,4,105000100,1 +12275,4,105000110,1 +12276,4,108000090,1 +12277,4,110000090,1 +12278,4,102000100,1 +12279,4,102000110,1 +12280,4,106000090,1 +12281,4,109000090,1 +12282,4,107000100,1 +12283,4,103000090,1 +12284,4,102000090,1 +12285,4,103000100,1 +12286,4,106000100,1 +12287,4,106000110,1 +12288,4,104000090,1 +12289,4,104000100,1 +12290,4,104000110,1 +12291,4,107000090,1 +12292,4,104000180,1 +12293,4,111000040,1 +12294,4,112000040,1 +12295,4,108000100,1 +12296,4,105000090,1 +12297,4,110000100,1 +12298,4,118000050,1 +12299,4,118000060,1 +12300,5,101000120,1 +12301,5,109000110,1 +12302,5,105000120,1 +12303,5,102000120,1 +12304,5,107000110,1 +12305,5,103000120,1 +12306,5,106000120,1 +12307,5,104000120,1 +12308,5,104000190,1 +12309,5,111000060,1 +12310,5,112000060,1 +12311,5,108000110,1 +12312,5,110000110,1 +12313,5,118000070,1 +12314,1,201000010,8 +12315,1,292000010,8 +12316,1,299000040,8 +12317,1,299000070,8 +12318,1,299000110,8 +12319,1,299000120,8 +12320,1,299000140,8 +12321,2,202000010,8 +12322,2,290000010,8 +12323,2,299000010,8 +12324,2,299000150,8 +12325,2,299000190,8 +12326,2,299000200,8 +12327,2,299000210,8 +12328,3,298000050,8 +12329,3,298000060,8 +12330,3,299000060,8 +12331,3,299000170,8 +12332,3,290000120,8 +12333,3,291000050,8 +12334,3,292000020,8 +12335,4,299000670,8 +12336,4,299000680,8 +12337,4,204000010,8 +12338,4,209000040,8 +12339,4,202000070,8 +12340,4,209000070,8 +12341,4,203000110,8 +12342,5,297000100,8 +12343,5,291000020,8 +12344,5,297000130,8 +12345,5,297000140,8 +12346,5,203000010,8 +12347,5,206000030,8 +12348,5,203000050,8 +12349,5,202000090,8 +12350,5,204000080,8 +12351,1,170002,3 +12352,1,180002,3 +12353,2,170003,3 +12354,2,180003,3 +12355,3,170004,3 +12356,3,180004,3 +12357,4,140000,3 +12358,5,150010,3 +12359,5,150020,3 +12360,5,150030,3 +12361,5,150040,3 +12363,1,101000010,1 +12364,1,102000010,1 +12365,1,103000010,1 +12366,1,104000010,1 +12367,1,105000010,1 +12368,1,106000010,1 +12369,1,107000010,1 +12370,1,108000010,1 +12371,1,109000010,1 +12372,1,110000010,1 +12373,2,101000020,1 +12374,2,101000030,1 +12375,2,102000020,1 +12376,2,102000030,1 +12377,2,102000040,1 +12378,2,103000020,1 +12379,2,103000030,1 +12380,2,103000040,1 +12381,2,104000020,1 +12382,2,104000030,1 +12383,2,104000040,1 +12384,2,105000020,1 +12385,2,105000030,1 +12386,2,105000040,1 +12387,2,106000020,1 +12388,2,106000030,1 +12389,2,106000040,1 +12390,2,107000020,1 +12391,2,107000030,1 +12392,2,107000040,1 +12393,2,108000020,1 +12394,2,108000030,1 +12395,2,108000040,1 +12396,2,109000020,1 +12397,2,109000030,1 +12398,2,109000040,1 +12399,2,110000020,1 +12400,2,110000030,1 +12401,2,110000040,1 +12402,2,118000010,1 +12403,3,101000050,1 +12404,3,101000060,1 +12405,3,101000080,1 +12406,3,101000040,1 +12407,3,109000060,1 +12408,3,109000070,1 +12409,3,109000080,1 +12410,3,105000060,1 +12411,3,105000070,1 +12412,3,105000080,1 +12413,3,104000050,1 +12414,3,106000050,1 +12415,3,102000060,1 +12416,3,102000070,1 +12417,3,102000080,1 +12418,3,103000050,1 +12419,3,105000050,1 +12420,3,107000060,1 +12421,3,107000070,1 +12422,3,107000080,1 +12423,3,108000050,1 +12424,3,109000050,1 +12425,3,103000060,1 +12426,3,103000070,1 +12427,3,103000080,1 +12428,3,110000050,1 +12429,3,106000060,1 +12430,3,106000070,1 +12431,3,106000080,1 +12432,3,101000070,1 +12433,3,110000060,1 +12434,3,104000060,1 +12435,3,104000070,1 +12436,3,104000080,1 +12437,3,102000050,1 +12438,3,104000170,1 +12439,3,104000260,1 +12440,3,111000010,1 +12441,3,111000020,1 +12442,3,111000030,1 +12443,3,112000020,1 +12444,3,112000030,1 +12445,3,108000060,1 +12446,3,108000070,1 +12447,3,108000080,1 +12448,3,107000050,1 +12449,3,112000010,1 +12450,3,110000070,1 +12451,3,110000080,1 +12452,3,118000020,1 +12453,3,118000030,1 +12454,3,118000040,1 +12455,4,101000090,1 +12456,4,101000100,1 +12457,4,101000110,1 +12458,4,109000100,1 +12459,4,105000100,1 +12460,4,105000110,1 +12461,4,108000090,1 +12462,4,110000090,1 +12463,4,102000100,1 +12464,4,102000110,1 +12465,4,106000090,1 +12466,4,109000090,1 +12467,4,107000100,1 +12468,4,103000090,1 +12469,4,102000090,1 +12470,4,103000100,1 +12471,4,106000100,1 +12472,4,106000110,1 +12473,4,104000090,1 +12474,4,104000100,1 +12475,4,104000110,1 +12476,4,107000090,1 +12477,4,104000180,1 +12478,4,111000040,1 +12479,4,112000040,1 +12480,4,108000100,1 +12481,4,105000090,1 +12482,4,110000100,1 +12483,4,118000050,1 +12484,4,118000060,1 +12485,5,101000120,1 +12486,5,109000110,1 +12487,5,105000120,1 +12488,5,102000120,1 +12489,5,107000110,1 +12490,5,103000120,1 +12491,5,106000120,1 +12492,5,104000120,1 +12493,5,104000190,1 +12494,5,111000060,1 +12495,5,112000060,1 +12496,5,108000110,1 +12497,5,110000110,1 +12498,5,118000070,1 +12499,1,201000010,8 +12500,1,292000010,8 +12501,1,299000040,8 +12502,1,299000070,8 +12503,1,299000110,8 +12504,1,299000120,8 +12505,1,299000140,8 +12506,2,202000010,8 +12507,2,290000010,8 +12508,2,299000010,8 +12509,2,299000150,8 +12510,2,299000190,8 +12511,2,299000200,8 +12512,2,299000210,8 +12513,3,298000050,8 +12514,3,298000060,8 +12515,3,299000060,8 +12516,3,299000170,8 +12517,3,290000120,8 +12518,3,291000050,8 +12519,3,292000020,8 +12520,4,299000670,8 +12521,4,299000680,8 +12522,4,204000010,8 +12523,4,209000040,8 +12524,4,202000070,8 +12525,4,209000070,8 +12526,4,203000110,8 +12527,5,297000100,8 +12528,5,291000020,8 +12529,5,297000130,8 +12530,5,297000140,8 +12531,5,203000010,8 +12532,5,206000030,8 +12533,5,203000050,8 +12534,5,202000090,8 +12535,5,204000080,8 +12536,2,170003,3 +12537,2,180003,3 +12538,3,170004,3 +12539,3,180004,3 +12540,4,140000,3 +12541,5,150010,3 +12542,5,150020,3 +12543,5,150030,3 +12544,5,150040,3 +12546,3,101000050,1 +12547,3,101000060,1 +12548,3,101000080,1 +12549,3,101000040,1 +12550,3,109000060,1 +12551,3,109000070,1 +12552,3,109000080,1 +12553,3,105000060,1 +12554,3,105000070,1 +12555,3,105000080,1 +12556,3,104000050,1 +12557,3,106000050,1 +12558,3,102000060,1 +12559,3,102000070,1 +12560,3,102000080,1 +12561,3,103000050,1 +12562,3,105000050,1 +12563,3,107000060,1 +12564,3,107000070,1 +12565,3,107000080,1 +12566,3,108000050,1 +12567,3,109000050,1 +12568,3,103000060,1 +12569,3,103000070,1 +12570,3,103000080,1 +12571,3,110000050,1 +12572,3,106000060,1 +12573,3,106000070,1 +12574,3,106000080,1 +12575,3,101000070,1 +12576,3,110000060,1 +12577,3,104000060,1 +12578,3,104000070,1 +12579,3,104000080,1 +12580,3,102000050,1 +12581,3,104000170,1 +12582,3,104000260,1 +12583,3,111000010,1 +12584,3,111000020,1 +12585,3,111000030,1 +12586,3,112000020,1 +12587,3,112000030,1 +12588,3,108000060,1 +12589,3,108000070,1 +12590,3,108000080,1 +12591,3,107000050,1 +12592,3,112000010,1 +12593,3,110000070,1 +12594,3,110000080,1 +12595,3,118000020,1 +12596,3,118000030,1 +12597,3,118000040,1 +12598,4,101000090,1 +12599,4,101000100,1 +12600,4,101000110,1 +12601,4,109000100,1 +12602,4,105000100,1 +12603,4,105000110,1 +12604,4,108000090,1 +12605,4,110000090,1 +12606,4,102000100,1 +12607,4,102000110,1 +12608,4,106000090,1 +12609,4,109000090,1 +12610,4,107000100,1 +12611,4,103000090,1 +12612,4,102000090,1 +12613,4,103000100,1 +12614,4,106000100,1 +12615,4,106000110,1 +12616,4,104000090,1 +12617,4,104000100,1 +12618,4,104000110,1 +12619,4,107000090,1 +12620,4,104000180,1 +12621,4,111000040,1 +12622,4,112000040,1 +12623,4,108000100,1 +12624,4,105000090,1 +12625,4,110000100,1 +12626,4,118000050,1 +12627,4,118000060,1 +12628,5,101000120,1 +12629,5,109000110,1 +12630,5,105000120,1 +12631,5,102000120,1 +12632,5,107000110,1 +12633,5,103000120,1 +12634,5,106000120,1 +12635,5,104000120,1 +12636,5,104000190,1 +12637,5,111000060,1 +12638,5,112000060,1 +12639,5,108000110,1 +12640,5,110000110,1 +12641,5,118000070,1 +12642,1,201000010,8 +12643,1,292000010,8 +12644,1,299000040,8 +12645,1,299000070,8 +12646,1,299000110,8 +12647,1,299000120,8 +12648,1,299000140,8 +12649,2,202000010,8 +12650,2,290000010,8 +12651,2,299000010,8 +12652,2,299000150,8 +12653,2,299000190,8 +12654,2,299000200,8 +12655,2,299000210,8 +12656,3,298000050,8 +12657,3,298000060,8 +12658,3,299000060,8 +12659,3,299000170,8 +12660,3,290000120,8 +12661,3,291000050,8 +12662,3,292000020,8 +12663,4,299000670,8 +12664,4,299000680,8 +12665,4,204000010,8 +12666,4,209000040,8 +12667,4,202000070,8 +12668,4,209000070,8 +12669,4,203000110,8 +12670,5,297000100,8 +12671,5,291000020,8 +12672,5,297000130,8 +12673,5,297000140,8 +12674,5,203000010,8 +12675,5,206000030,8 +12676,5,203000050,8 +12677,5,202000090,8 +12678,5,204000080,8 +12679,3,170004,3 +12680,3,180004,3 +12681,4,140000,3 +12682,5,150010,3 +12683,5,150020,3 +12684,5,150030,3 +12685,5,150040,3 +12687,3,101000050,1 +12688,3,101000060,1 +12689,3,101000080,1 +12690,3,101000040,1 +12691,3,109000060,1 +12692,3,109000070,1 +12693,3,109000080,1 +12694,3,105000060,1 +12695,3,105000070,1 +12696,3,105000080,1 +12697,3,104000050,1 +12698,3,106000050,1 +12699,3,102000060,1 +12700,3,102000070,1 +12701,3,102000080,1 +12702,3,103000050,1 +12703,3,105000050,1 +12704,3,107000060,1 +12705,3,107000070,1 +12706,3,107000080,1 +12707,3,108000050,1 +12708,3,109000050,1 +12709,3,103000060,1 +12710,3,103000070,1 +12711,3,103000080,1 +12712,3,110000050,1 +12713,3,106000060,1 +12714,3,106000070,1 +12715,3,106000080,1 +12716,3,101000070,1 +12717,3,110000060,1 +12718,3,104000060,1 +12719,3,104000070,1 +12720,3,104000080,1 +12721,3,102000050,1 +12722,3,104000170,1 +12723,3,104000260,1 +12724,3,111000010,1 +12725,3,111000020,1 +12726,3,111000030,1 +12727,3,112000020,1 +12728,3,112000030,1 +12729,3,108000060,1 +12730,3,108000070,1 +12731,3,108000080,1 +12732,3,107000050,1 +12733,3,112000010,1 +12734,3,110000070,1 +12735,3,110000080,1 +12736,3,118000020,1 +12737,3,118000030,1 +12738,3,118000040,1 +12739,4,101000090,1 +12740,4,101000100,1 +12741,4,101000110,1 +12742,4,109000100,1 +12743,4,105000100,1 +12744,4,105000110,1 +12745,4,108000090,1 +12746,4,110000090,1 +12747,4,102000100,1 +12748,4,102000110,1 +12749,4,106000090,1 +12750,4,109000090,1 +12751,4,107000100,1 +12752,4,103000090,1 +12753,4,102000090,1 +12754,4,103000100,1 +12755,4,106000100,1 +12756,4,106000110,1 +12757,4,104000090,1 +12758,4,104000100,1 +12759,4,104000110,1 +12760,4,107000090,1 +12761,4,104000180,1 +12762,4,111000040,1 +12763,4,112000040,1 +12764,4,108000100,1 +12765,4,105000090,1 +12766,4,110000100,1 +12767,4,118000050,1 +12768,4,118000060,1 +12769,5,101000120,1 +12770,5,109000110,1 +12771,5,105000120,1 +12772,5,102000120,1 +12773,5,107000110,1 +12774,5,103000120,1 +12775,5,106000120,1 +12776,5,104000120,1 +12777,5,104000190,1 +12778,5,111000060,1 +12779,5,112000060,1 +12780,5,108000110,1 +12781,5,110000110,1 +12782,5,118000070,1 +12783,1,201000010,8 +12784,1,292000010,8 +12785,1,299000040,8 +12786,1,299000070,8 +12787,1,299000110,8 +12788,1,299000120,8 +12789,1,299000140,8 +12790,2,202000010,8 +12791,2,290000010,8 +12792,2,299000010,8 +12793,2,299000150,8 +12794,2,299000190,8 +12795,2,299000200,8 +12796,2,299000210,8 +12797,3,298000050,8 +12798,3,298000060,8 +12799,3,299000060,8 +12800,3,299000170,8 +12801,3,290000120,8 +12802,3,291000050,8 +12803,3,292000020,8 +12804,4,299000670,8 +12805,4,299000680,8 +12806,4,204000010,8 +12807,4,209000040,8 +12808,4,202000070,8 +12809,4,209000070,8 +12810,4,203000110,8 +12811,5,297000100,8 +12812,5,291000020,8 +12813,5,297000130,8 +12814,5,297000140,8 +12815,5,203000010,8 +12816,5,206000030,8 +12817,5,203000050,8 +12818,5,202000090,8 +12819,5,204000080,8 +12820,3,170004,3 +12821,3,180004,3 +12822,4,140000,3 +12823,5,150010,3 +12824,5,150020,3 +12825,5,150030,3 +12826,5,150040,3 +12828,3,101000050,1 +12829,3,101000060,1 +12830,3,101000080,1 +12831,3,101000040,1 +12832,3,109000060,1 +12833,3,109000070,1 +12834,3,109000080,1 +12835,3,105000060,1 +12836,3,105000070,1 +12837,3,105000080,1 +12838,3,104000050,1 +12839,3,106000050,1 +12840,3,102000060,1 +12841,3,102000070,1 +12842,3,102000080,1 +12843,3,103000050,1 +12844,3,105000050,1 +12845,3,107000060,1 +12846,3,107000070,1 +12847,3,107000080,1 +12848,3,108000050,1 +12849,3,109000050,1 +12850,3,103000060,1 +12851,3,103000070,1 +12852,3,103000080,1 +12853,3,110000050,1 +12854,3,106000060,1 +12855,3,106000070,1 +12856,3,106000080,1 +12857,3,101000070,1 +12858,3,110000060,1 +12859,3,104000060,1 +12860,3,104000070,1 +12861,3,104000080,1 +12862,3,102000050,1 +12863,3,104000170,1 +12864,3,104000260,1 +12865,3,111000010,1 +12866,3,111000020,1 +12867,3,111000030,1 +12868,3,112000020,1 +12869,3,112000030,1 +12870,3,108000060,1 +12871,3,108000070,1 +12872,3,108000080,1 +12873,3,107000050,1 +12874,3,112000010,1 +12875,3,110000070,1 +12876,3,110000080,1 +12877,3,118000020,1 +12878,3,118000030,1 +12879,3,118000040,1 +12880,4,101000090,1 +12881,4,101000100,1 +12882,4,101000110,1 +12883,4,109000100,1 +12884,4,105000100,1 +12885,4,105000110,1 +12886,4,108000090,1 +12887,4,110000090,1 +12888,4,102000100,1 +12889,4,102000110,1 +12890,4,106000090,1 +12891,4,109000090,1 +12892,4,107000100,1 +12893,4,103000090,1 +12894,4,102000090,1 +12895,4,103000100,1 +12896,4,106000100,1 +12897,4,106000110,1 +12898,4,104000090,1 +12899,4,104000100,1 +12900,4,104000110,1 +12901,4,107000090,1 +12902,4,104000180,1 +12903,4,111000040,1 +12904,4,112000040,1 +12905,4,108000100,1 +12906,4,105000090,1 +12907,4,110000100,1 +12908,4,118000050,1 +12909,4,118000060,1 +12910,5,101000120,1 +12911,5,109000110,1 +12912,5,105000120,1 +12913,5,102000120,1 +12914,5,107000110,1 +12915,5,103000120,1 +12916,5,106000120,1 +12917,5,104000120,1 +12918,5,104000190,1 +12919,5,111000060,1 +12920,5,112000060,1 +12921,5,108000110,1 +12922,5,110000110,1 +12923,5,118000070,1 +12924,1,201000010,8 +12925,1,292000010,8 +12926,1,299000040,8 +12927,1,299000070,8 +12928,1,299000110,8 +12929,1,299000120,8 +12930,1,299000140,8 +12931,2,202000010,8 +12932,2,290000010,8 +12933,2,299000010,8 +12934,2,299000150,8 +12935,2,299000190,8 +12936,2,299000200,8 +12937,2,299000210,8 +12938,3,298000050,8 +12939,3,298000060,8 +12940,3,299000060,8 +12941,3,299000170,8 +12942,3,290000120,8 +12943,3,291000050,8 +12944,3,292000020,8 +12945,4,299000670,8 +12946,4,299000680,8 +12947,4,204000010,8 +12948,4,209000040,8 +12949,4,202000070,8 +12950,4,209000070,8 +12951,4,203000110,8 +12952,5,297000100,8 +12953,5,291000020,8 +12954,5,297000130,8 +12955,5,297000140,8 +12956,5,203000010,8 +12957,5,206000030,8 +12958,5,203000050,8 +12959,5,202000090,8 +12960,5,204000080,8 +12961,3,170004,3 +12962,3,180004,3 +12963,4,140000,3 +12964,5,150010,3 +12965,5,150020,3 +12966,5,150030,3 +12967,5,150040,3 +12968,5,190000,3 +12969,5,200000,3 +12970,5,210000,3 +12972,3,102000060,1 +12973,3,102000070,1 +12974,3,102000080,1 +12975,3,103000050,1 +12976,3,105000050,1 +12977,3,107000060,1 +12978,3,107000070,1 +12979,3,107000080,1 +12980,3,108000050,1 +12981,3,109000050,1 +12982,3,103000060,1 +12983,3,103000070,1 +12984,3,103000080,1 +12985,3,110000050,1 +12986,4,102000100,1 +12987,4,102000110,1 +12988,4,102000350,1 +12989,4,106000090,1 +12990,4,109000090,1 +12991,4,107000100,1 +12992,4,103000090,1 +12993,4,102000090,1 +12994,4,103000100,1 +12995,5,102000120,1 +12996,5,107000110,1 +12997,5,103000120,1 +12998,1,102000000,2 +12999,2,102000001,2 +13000,3,102000002,2 +13001,4,102000003,2 +13002,5,102000004,2 +13003,1,105000000,2 +13004,2,105000001,2 +13005,3,105000002,2 +13006,4,105000003,2 +13007,5,105000004,2 +13008,1,112000000,2 +13009,2,112000001,2 +13010,3,112000002,2 +13011,4,112000003,2 +13012,5,112000004,2 +13013,3,170004,3 +13014,4,170005,3 +13015,3,180004,3 +13016,4,180005,3 +13017,4,140000,3 +13018,4,150010,3 +13019,4,150020,3 +13020,4,150030,3 +13021,4,150040,3 +13023,3,118000020,1 +13024,3,118000030,1 +13025,3,118000040,1 +13026,3,106000060,1 +13027,3,106000070,1 +13028,3,106000080,1 +13029,3,101000070,1 +13030,3,110000060,1 +13031,3,104000060,1 +13032,3,104000070,1 +13033,3,104000080,1 +13034,3,102000050,1 +13035,3,104000170,1 +13036,3,104000260,1 +13037,4,118000050,1 +13038,4,118000060,1 +13039,4,106000100,1 +13040,4,106000110,1 +13041,4,104000090,1 +13042,4,104000100,1 +13043,4,104000110,1 +13044,4,104000270,1 +13045,4,107000090,1 +13046,4,104000180,1 +13047,5,118000070,1 +13048,5,106000120,1 +13049,5,104000120,1 +13050,5,104000190,1 +13051,1,103000000,2 +13052,2,103000001,2 +13053,3,103000002,2 +13054,4,103000003,2 +13055,5,103000004,2 +13056,1,111000000,2 +13057,2,111000001,2 +13058,3,111000002,2 +13059,4,111000003,2 +13060,5,111000004,2 +13061,1,115000000,2 +13062,2,115000001,2 +13063,3,115000002,2 +13064,4,115000003,2 +13065,5,115000004,2 +13066,4,110024,3 +13067,4,110034,3 +13068,4,110044,3 +13069,4,110054,3 +13070,3,110060,3 +13071,3,110070,3 +13072,3,110080,3 +13073,3,110090,3 +13074,3,110100,3 +13075,3,110110,3 +13076,3,110120,3 +13077,3,110130,3 +13078,3,110140,3 +13079,3,110150,3 +13080,3,110160,3 +13081,3,110170,3 +13082,3,110180,3 +13083,3,110190,3 +13084,3,110200,3 +13085,3,110210,3 +13086,3,110220,3 +13087,3,110230,3 +13088,3,110240,3 +13089,3,110250,3 +13090,3,110260,3 +13091,3,110270,3 +13092,3,110620,3 +13093,3,110670,3 +13094,4,140000,3 +13095,4,150010,3 +13096,4,150020,3 +13097,4,150030,3 +13098,4,150040,3 +13100,3,111000010,1 +13101,3,111000020,1 +13102,3,111000030,1 +13103,3,112000020,1 +13104,3,112000030,1 +13105,3,108000060,1 +13106,3,108000070,1 +13107,3,108000080,1 +13108,3,107000050,1 +13109,3,112000010,1 +13110,3,110000070,1 +13111,3,110000080,1 +13112,4,111000040,1 +13113,4,112000040,1 +13114,4,108000100,1 +13115,4,105000090,1 +13116,4,110000100,1 +13117,5,111000060,1 +13118,5,112000060,1 +13119,5,108000110,1 +13120,5,110000110,1 +13121,1,108000000,2 +13122,2,108000001,2 +13123,3,108000002,2 +13124,4,108000003,2 +13125,5,108000004,2 +13126,1,107000000,2 +13127,2,107000001,2 +13128,3,107000002,2 +13129,4,107000003,2 +13130,5,107000004,2 +13131,1,120000000,2 +13132,2,120000001,2 +13133,3,120000002,2 +13134,4,120000003,2 +13135,5,120000004,2 +13136,4,120024,3 +13137,4,120034,3 +13138,4,120044,3 +13139,4,120054,3 +13140,3,120241,3 +13141,3,120251,3 +13142,3,120261,3 +13143,3,120271,3 +13144,3,120300,3 +13145,3,120310,3 +13146,3,120320,3 +13147,3,120330,3 +13148,3,120340,3 +13149,3,120350,3 +13150,3,120360,3 +13151,3,120370,3 +13152,3,120380,3 +13153,3,120390,3 +13154,3,120400,3 +13155,3,120410,3 +13156,3,120420,3 +13157,3,120430,3 +13158,3,120450,3 +13159,3,120460,3 +13160,3,120550,3 +13161,3,120560,3 +13162,3,120570,3 +13163,3,120990,3 +13164,3,121000,3 +13165,3,121010,3 +13166,3,121020,3 +13167,3,121100,3 +13168,4,140000,3 +13169,4,150010,3 +13170,4,150020,3 +13171,4,150030,3 +13172,4,150040,3 +13174,3,101000050,1 +13175,3,101000060,1 +13176,3,101000080,1 +13177,3,101000040,1 +13178,3,109000060,1 +13179,3,109000070,1 +13180,3,109000080,1 +13181,3,105000060,1 +13182,3,105000070,1 +13183,3,105000080,1 +13184,3,104000050,1 +13185,3,106000050,1 +13186,4,101000090,1 +13187,4,101000100,1 +13188,4,101000110,1 +13189,4,109000100,1 +13190,4,105000100,1 +13191,4,105000110,1 +13192,4,108000090,1 +13193,4,110000090,1 +13194,5,101000120,1 +13195,5,109000110,1 +13196,5,105000120,1 +13197,1,101000000,2 +13198,2,101000001,2 +13199,3,101000002,2 +13200,4,101000008,2 +13201,5,101000004,2 +13202,1,109000000,2 +13203,2,109000001,2 +13204,3,109000002,2 +13205,4,109000003,2 +13206,5,109000004,2 +13207,4,130024,3 +13208,4,130034,3 +13209,4,130044,3 +13210,4,130054,3 +13211,3,130060,3 +13212,3,130070,3 +13213,3,130080,3 +13214,3,130090,3 +13215,3,130100,3 +13216,3,130110,3 +13217,3,130120,3 +13218,3,130130,3 +13219,3,130140,3 +13220,3,130150,3 +13221,3,130160,3 +13222,3,130170,3 +13223,3,130180,3 +13224,3,130190,3 +13225,3,130200,3 +13226,3,130420,3 +13227,3,130510,3 +13228,3,130520,3 +13229,3,130531,3 +13230,3,130540,3 +13231,3,130660,3 +13232,3,130700,3 +13233,3,130790,3 +13234,3,130800,3 +13235,3,131130,3 +13236,4,140000,3 +13237,4,150010,3 +13238,4,150020,3 +13239,4,150030,3 +13240,4,150040,3 +13242,3,101000050,1 +13243,3,101000060,1 +13244,3,101000080,1 +13245,3,101000040,1 +13246,3,109000060,1 +13247,3,109000070,1 +13248,3,109000080,1 +13249,3,105000060,1 +13250,3,105000070,1 +13251,3,105000080,1 +13252,3,104000050,1 +13253,3,106000050,1 +13254,4,101000090,1 +13255,4,101000100,1 +13256,4,101000110,1 +13257,4,109000100,1 +13258,4,105000100,1 +13259,4,105000110,1 +13260,4,108000090,1 +13261,4,110000090,1 +13262,5,101000120,1 +13263,5,109000110,1 +13264,5,105000120,1 +13265,1,101000000,2 +13266,2,101000001,2 +13267,3,101000002,2 +13268,4,101000008,2 +13269,5,101000004,2 +13270,1,109000000,2 +13271,2,109000001,2 +13272,3,109000002,2 +13273,4,109000003,2 +13274,5,109000004,2 +13275,3,170004,3 +13276,4,170005,3 +13277,3,180004,3 +13278,4,180005,3 +13279,4,140000,3 +13280,4,150010,3 +13281,4,150020,3 +13282,4,150030,3 +13283,4,150040,3 +13285,3,102000060,1 +13286,3,102000070,1 +13287,3,102000080,1 +13288,3,103000050,1 +13289,3,105000050,1 +13290,3,107000060,1 +13291,3,107000070,1 +13292,3,107000080,1 +13293,3,108000050,1 +13294,3,109000050,1 +13295,3,103000060,1 +13296,3,103000070,1 +13297,3,103000080,1 +13298,3,110000050,1 +13299,4,102000100,1 +13300,4,102000110,1 +13301,4,102000350,1 +13302,4,106000090,1 +13303,4,109000090,1 +13304,4,107000100,1 +13305,4,103000090,1 +13306,4,102000090,1 +13307,4,103000100,1 +13308,5,102000120,1 +13309,5,107000110,1 +13310,5,103000120,1 +13311,1,102000000,2 +13312,2,102000001,2 +13313,3,102000002,2 +13314,4,102000003,2 +13315,5,102000004,2 +13316,1,105000000,2 +13317,2,105000001,2 +13318,3,105000002,2 +13319,4,105000003,2 +13320,5,105000004,2 +13321,1,112000000,2 +13322,2,112000001,2 +13323,3,112000002,2 +13324,4,112000003,2 +13325,5,112000004,2 +13326,4,110024,3 +13327,4,110034,3 +13328,4,110044,3 +13329,4,110054,3 +13330,3,110060,3 +13331,3,110070,3 +13332,3,110080,3 +13333,3,110090,3 +13334,3,110100,3 +13335,3,110110,3 +13336,3,110120,3 +13337,3,110130,3 +13338,3,110140,3 +13339,3,110150,3 +13340,3,110160,3 +13341,3,110170,3 +13342,3,110180,3 +13343,3,110190,3 +13344,3,110200,3 +13345,3,110210,3 +13346,3,110220,3 +13347,3,110230,3 +13348,3,110240,3 +13349,3,110250,3 +13350,3,110260,3 +13351,3,110270,3 +13352,3,110620,3 +13353,3,110670,3 +13354,4,140000,3 +13355,4,150010,3 +13356,4,150020,3 +13357,4,150030,3 +13358,4,150040,3 +13360,3,118000020,1 +13361,3,118000030,1 +13362,3,118000040,1 +13363,3,106000060,1 +13364,3,106000070,1 +13365,3,106000080,1 +13366,3,101000070,1 +13367,3,110000060,1 +13368,3,104000060,1 +13369,3,104000070,1 +13370,3,104000080,1 +13371,3,102000050,1 +13372,3,104000170,1 +13373,3,104000260,1 +13374,4,118000050,1 +13375,4,118000060,1 +13376,4,106000100,1 +13377,4,106000110,1 +13378,4,104000090,1 +13379,4,104000100,1 +13380,4,104000110,1 +13381,4,104000270,1 +13382,4,107000090,1 +13383,4,104000180,1 +13384,5,118000070,1 +13385,5,106000120,1 +13386,5,104000120,1 +13387,5,104000190,1 +13388,1,103000000,2 +13389,2,103000001,2 +13390,3,103000002,2 +13391,4,103000003,2 +13392,5,103000004,2 +13393,1,111000000,2 +13394,2,111000001,2 +13395,3,111000002,2 +13396,4,111000003,2 +13397,5,111000004,2 +13398,1,115000000,2 +13399,2,115000001,2 +13400,3,115000002,2 +13401,4,115000003,2 +13402,5,115000004,2 +13403,4,120024,3 +13404,4,120034,3 +13405,4,120044,3 +13406,4,120054,3 +13407,3,120241,3 +13408,3,120251,3 +13409,3,120261,3 +13410,3,120271,3 +13411,3,120300,3 +13412,3,120310,3 +13413,3,120320,3 +13414,3,120330,3 +13415,3,120340,3 +13416,3,120350,3 +13417,3,120360,3 +13418,3,120370,3 +13419,3,120380,3 +13420,3,120390,3 +13421,3,120400,3 +13422,3,120410,3 +13423,3,120420,3 +13424,3,120430,3 +13425,3,120450,3 +13426,3,120460,3 +13427,3,120550,3 +13428,3,120560,3 +13429,3,120570,3 +13430,3,120990,3 +13431,3,121000,3 +13432,3,121010,3 +13433,3,121020,3 +13434,3,121100,3 +13435,4,140000,3 +13436,4,150010,3 +13437,4,150020,3 +13438,4,150030,3 +13439,4,150040,3 +13441,3,111000010,1 +13442,3,111000020,1 +13443,3,111000030,1 +13444,3,112000020,1 +13445,3,112000030,1 +13446,3,108000060,1 +13447,3,108000070,1 +13448,3,108000080,1 +13449,3,107000050,1 +13450,3,112000010,1 +13451,3,110000070,1 +13452,3,110000080,1 +13453,4,111000040,1 +13454,4,112000040,1 +13455,4,108000100,1 +13456,4,105000090,1 +13457,4,110000100,1 +13458,5,111000060,1 +13459,5,112000060,1 +13460,5,108000110,1 +13461,5,110000110,1 +13462,1,108000000,2 +13463,2,108000001,2 +13464,3,108000002,2 +13465,4,108000003,2 +13466,5,108000004,2 +13467,1,107000000,2 +13468,2,107000001,2 +13469,3,107000002,2 +13470,4,107000003,2 +13471,5,107000004,2 +13472,1,120000000,2 +13473,2,120000001,2 +13474,3,120000002,2 +13475,4,120000003,2 +13476,5,120000004,2 +13477,4,130024,3 +13478,4,130034,3 +13479,4,130044,3 +13480,4,130054,3 +13481,3,130060,3 +13482,3,130070,3 +13483,3,130080,3 +13484,3,130090,3 +13485,3,130100,3 +13486,3,130110,3 +13487,3,130120,3 +13488,3,130130,3 +13489,3,130140,3 +13490,3,130150,3 +13491,3,130160,3 +13492,3,130170,3 +13493,3,130180,3 +13494,3,130190,3 +13495,3,130200,3 +13496,3,130420,3 +13497,3,130510,3 +13498,3,130520,3 +13499,3,130531,3 +13500,3,130540,3 +13501,3,130660,3 +13502,3,130700,3 +13503,3,130790,3 +13504,3,130800,3 +13505,3,131130,3 +13506,4,140000,3 +13507,4,150010,3 +13508,4,150020,3 +13509,4,150030,3 +13510,4,150040,3 +13512,1,101000010,1 +13513,1,102000010,1 +13514,1,103000010,1 +13515,1,104000010,1 +13516,1,105000010,1 +13517,1,106000010,1 +13518,1,107000010,1 +13519,1,108000010,1 +13520,1,109000010,1 +13521,1,110000010,1 +13522,2,101000020,1 +13523,2,101000030,1 +13524,2,102000020,1 +13525,2,102000030,1 +13526,2,102000040,1 +13527,2,103000020,1 +13528,2,103000030,1 +13529,2,103000040,1 +13530,2,104000020,1 +13531,2,104000030,1 +13532,2,104000040,1 +13533,2,105000020,1 +13534,2,105000030,1 +13535,2,105000040,1 +13536,2,106000020,1 +13537,2,106000030,1 +13538,2,106000040,1 +13539,2,107000020,1 +13540,2,107000030,1 +13541,2,107000040,1 +13542,2,108000020,1 +13543,2,108000030,1 +13544,2,108000040,1 +13545,2,109000020,1 +13546,2,109000030,1 +13547,2,109000040,1 +13548,2,110000020,1 +13549,2,110000030,1 +13550,2,110000040,1 +13551,2,118000010,1 +13552,3,101000050,1 +13553,3,101000060,1 +13554,3,101000080,1 +13555,3,101000040,1 +13556,3,109000060,1 +13557,3,109000070,1 +13558,3,109000080,1 +13559,3,105000060,1 +13560,3,105000070,1 +13561,3,105000080,1 +13562,3,104000050,1 +13563,3,106000050,1 +13564,3,102000060,1 +13565,3,102000070,1 +13566,3,102000080,1 +13567,3,103000050,1 +13568,3,105000050,1 +13569,3,107000060,1 +13570,3,107000070,1 +13571,3,107000080,1 +13572,3,108000050,1 +13573,3,109000050,1 +13574,3,103000060,1 +13575,3,103000070,1 +13576,3,103000080,1 +13577,3,110000050,1 +13578,3,106000060,1 +13579,3,106000070,1 +13580,3,106000080,1 +13581,3,101000070,1 +13582,3,110000060,1 +13583,3,104000060,1 +13584,3,104000070,1 +13585,3,104000080,1 +13586,3,102000050,1 +13587,3,104000170,1 +13588,3,104000260,1 +13589,3,111000010,1 +13590,3,111000020,1 +13591,3,111000030,1 +13592,3,112000020,1 +13593,3,112000030,1 +13594,3,108000060,1 +13595,3,108000070,1 +13596,3,108000080,1 +13597,3,107000050,1 +13598,3,112000010,1 +13599,3,110000070,1 +13600,3,110000080,1 +13601,3,118000020,1 +13602,3,118000030,1 +13603,3,118000040,1 +13604,4,101000090,1 +13605,4,101000100,1 +13606,4,101000110,1 +13607,4,109000100,1 +13608,4,105000100,1 +13609,4,105000110,1 +13610,4,108000090,1 +13611,4,110000090,1 +13612,4,102000100,1 +13613,4,102000110,1 +13614,4,106000090,1 +13615,4,109000090,1 +13616,4,107000100,1 +13617,4,103000090,1 +13618,4,102000090,1 +13619,4,103000100,1 +13620,4,106000100,1 +13621,4,106000110,1 +13622,4,104000090,1 +13623,4,104000100,1 +13624,4,104000110,1 +13625,4,107000090,1 +13626,4,104000180,1 +13627,4,111000040,1 +13628,4,112000040,1 +13629,4,108000100,1 +13630,4,105000090,1 +13631,4,110000100,1 +13632,4,118000050,1 +13633,4,118000060,1 +13634,5,101000120,1 +13635,5,109000110,1 +13636,5,105000120,1 +13637,5,102000120,1 +13638,5,107000110,1 +13639,5,103000120,1 +13640,5,106000120,1 +13641,5,104000120,1 +13642,5,104000190,1 +13643,5,111000060,1 +13644,5,112000060,1 +13645,5,108000110,1 +13646,5,110000110,1 +13647,5,118000070,1 +13648,1,201000010,8 +13649,1,292000010,8 +13650,1,299000040,8 +13651,1,299000070,8 +13652,1,299000110,8 +13653,1,299000120,8 +13654,1,299000140,8 +13655,2,202000010,8 +13656,2,290000010,8 +13657,2,299000010,8 +13658,2,299000150,8 +13659,2,299000190,8 +13660,2,299000200,8 +13661,2,299000210,8 +13662,3,298000050,8 +13663,3,298000060,8 +13664,3,299000060,8 +13665,3,299000170,8 +13666,3,290000120,8 +13667,3,291000050,8 +13668,3,292000020,8 +13669,4,299000670,8 +13670,4,299000680,8 +13671,4,204000010,8 +13672,4,209000040,8 +13673,4,202000070,8 +13674,4,209000070,8 +13675,4,203000110,8 +13676,4,290000110,8 +13677,5,297000100,8 +13678,5,291000020,8 +13679,5,297000130,8 +13680,5,297000140,8 +13681,5,203000010,8 +13682,5,206000030,8 +13683,5,203000050,8 +13684,5,202000090,8 +13685,5,204000080,8 +13686,5,202000150,8 +13687,1,170002,3 +13688,1,180002,3 +13689,2,170003,3 +13690,2,180003,3 +13691,3,170004,3 +13692,3,180004,3 +13693,4,140000,3 +13694,5,150010,3 +13695,5,150020,3 +13696,5,150030,3 +13697,5,150040,3 +13699,1,101000010,1 +13700,1,102000010,1 +13701,1,103000010,1 +13702,1,104000010,1 +13703,1,105000010,1 +13704,1,106000010,1 +13705,1,107000010,1 +13706,1,108000010,1 +13707,1,109000010,1 +13708,1,110000010,1 +13709,2,101000020,1 +13710,2,101000030,1 +13711,2,102000020,1 +13712,2,102000030,1 +13713,2,102000040,1 +13714,2,103000020,1 +13715,2,103000030,1 +13716,2,103000040,1 +13717,2,104000020,1 +13718,2,104000030,1 +13719,2,104000040,1 +13720,2,105000020,1 +13721,2,105000030,1 +13722,2,105000040,1 +13723,2,106000020,1 +13724,2,106000030,1 +13725,2,106000040,1 +13726,2,107000020,1 +13727,2,107000030,1 +13728,2,107000040,1 +13729,2,108000020,1 +13730,2,108000030,1 +13731,2,108000040,1 +13732,2,109000020,1 +13733,2,109000030,1 +13734,2,109000040,1 +13735,2,110000020,1 +13736,2,110000030,1 +13737,2,110000040,1 +13738,2,118000010,1 +13739,3,101000050,1 +13740,3,101000060,1 +13741,3,101000080,1 +13742,3,101000040,1 +13743,3,109000060,1 +13744,3,109000070,1 +13745,3,109000080,1 +13746,3,105000060,1 +13747,3,105000070,1 +13748,3,105000080,1 +13749,3,104000050,1 +13750,3,106000050,1 +13751,3,102000060,1 +13752,3,102000070,1 +13753,3,102000080,1 +13754,3,103000050,1 +13755,3,105000050,1 +13756,3,107000060,1 +13757,3,107000070,1 +13758,3,107000080,1 +13759,3,108000050,1 +13760,3,109000050,1 +13761,3,103000060,1 +13762,3,103000070,1 +13763,3,103000080,1 +13764,3,110000050,1 +13765,3,106000060,1 +13766,3,106000070,1 +13767,3,106000080,1 +13768,3,101000070,1 +13769,3,110000060,1 +13770,3,104000060,1 +13771,3,104000070,1 +13772,3,104000080,1 +13773,3,102000050,1 +13774,3,104000170,1 +13775,3,104000260,1 +13776,3,111000010,1 +13777,3,111000020,1 +13778,3,111000030,1 +13779,3,112000020,1 +13780,3,112000030,1 +13781,3,108000060,1 +13782,3,108000070,1 +13783,3,108000080,1 +13784,3,107000050,1 +13785,3,112000010,1 +13786,3,110000070,1 +13787,3,110000080,1 +13788,3,118000020,1 +13789,3,118000030,1 +13790,3,118000040,1 +13791,4,101000090,1 +13792,4,101000100,1 +13793,4,101000110,1 +13794,4,109000100,1 +13795,4,105000100,1 +13796,4,105000110,1 +13797,4,108000090,1 +13798,4,110000090,1 +13799,4,102000100,1 +13800,4,102000110,1 +13801,4,106000090,1 +13802,4,109000090,1 +13803,4,107000100,1 +13804,4,103000090,1 +13805,4,102000090,1 +13806,4,103000100,1 +13807,4,106000100,1 +13808,4,106000110,1 +13809,4,104000090,1 +13810,4,104000100,1 +13811,4,104000110,1 +13812,4,107000090,1 +13813,4,104000180,1 +13814,4,111000040,1 +13815,4,112000040,1 +13816,4,108000100,1 +13817,4,105000090,1 +13818,4,110000100,1 +13819,4,118000050,1 +13820,4,118000060,1 +13821,5,101000120,1 +13822,5,109000110,1 +13823,5,105000120,1 +13824,5,102000120,1 +13825,5,107000110,1 +13826,5,103000120,1 +13827,5,106000120,1 +13828,5,104000120,1 +13829,5,104000190,1 +13830,5,111000060,1 +13831,5,112000060,1 +13832,5,108000110,1 +13833,5,110000110,1 +13834,5,118000070,1 +13835,1,201000010,8 +13836,1,292000010,8 +13837,1,299000040,8 +13838,1,299000070,8 +13839,1,299000110,8 +13840,1,299000120,8 +13841,1,299000140,8 +13842,2,202000010,8 +13843,2,290000010,8 +13844,2,299000010,8 +13845,2,299000150,8 +13846,2,299000190,8 +13847,2,299000200,8 +13848,2,299000210,8 +13849,3,298000050,8 +13850,3,298000060,8 +13851,3,299000060,8 +13852,3,299000170,8 +13853,3,290000120,8 +13854,3,291000050,8 +13855,3,292000020,8 +13856,4,299000670,8 +13857,4,299000680,8 +13858,4,204000010,8 +13859,4,209000040,8 +13860,4,202000070,8 +13861,4,209000070,8 +13862,4,203000110,8 +13863,4,290000110,8 +13864,5,297000100,8 +13865,5,291000020,8 +13866,5,297000130,8 +13867,5,297000140,8 +13868,5,203000010,8 +13869,5,206000030,8 +13870,5,203000050,8 +13871,5,202000090,8 +13872,5,204000080,8 +13873,5,202000150,8 +13874,2,170003,3 +13875,2,180003,3 +13876,3,170004,3 +13877,3,180004,3 +13878,4,140000,3 +13879,5,150010,3 +13880,5,150020,3 +13881,5,150030,3 +13882,5,150040,3 +13884,3,101000050,1 +13885,3,101000060,1 +13886,3,101000080,1 +13887,3,101000040,1 +13888,3,109000060,1 +13889,3,109000070,1 +13890,3,109000080,1 +13891,3,105000060,1 +13892,3,105000070,1 +13893,3,105000080,1 +13894,3,104000050,1 +13895,3,106000050,1 +13896,3,102000060,1 +13897,3,102000070,1 +13898,3,102000080,1 +13899,3,103000050,1 +13900,3,105000050,1 +13901,3,107000060,1 +13902,3,107000070,1 +13903,3,107000080,1 +13904,3,108000050,1 +13905,3,109000050,1 +13906,3,103000060,1 +13907,3,103000070,1 +13908,3,103000080,1 +13909,3,110000050,1 +13910,3,106000060,1 +13911,3,106000070,1 +13912,3,106000080,1 +13913,3,101000070,1 +13914,3,110000060,1 +13915,3,104000060,1 +13916,3,104000070,1 +13917,3,104000080,1 +13918,3,102000050,1 +13919,3,104000170,1 +13920,3,104000260,1 +13921,3,111000010,1 +13922,3,111000020,1 +13923,3,111000030,1 +13924,3,112000020,1 +13925,3,112000030,1 +13926,3,108000060,1 +13927,3,108000070,1 +13928,3,108000080,1 +13929,3,107000050,1 +13930,3,112000010,1 +13931,3,110000070,1 +13932,3,110000080,1 +13933,3,118000020,1 +13934,3,118000030,1 +13935,3,118000040,1 +13936,4,101000090,1 +13937,4,101000100,1 +13938,4,101000110,1 +13939,4,109000100,1 +13940,4,105000100,1 +13941,4,105000110,1 +13942,4,108000090,1 +13943,4,110000090,1 +13944,4,102000100,1 +13945,4,102000110,1 +13946,4,106000090,1 +13947,4,109000090,1 +13948,4,107000100,1 +13949,4,103000090,1 +13950,4,102000090,1 +13951,4,103000100,1 +13952,4,106000100,1 +13953,4,106000110,1 +13954,4,104000090,1 +13955,4,104000100,1 +13956,4,104000110,1 +13957,4,107000090,1 +13958,4,104000180,1 +13959,4,111000040,1 +13960,4,112000040,1 +13961,4,108000100,1 +13962,4,105000090,1 +13963,4,110000100,1 +13964,4,118000050,1 +13965,4,118000060,1 +13966,5,101000120,1 +13967,5,109000110,1 +13968,5,105000120,1 +13969,5,102000120,1 +13970,5,107000110,1 +13971,5,103000120,1 +13972,5,106000120,1 +13973,5,104000120,1 +13974,5,104000190,1 +13975,5,111000060,1 +13976,5,112000060,1 +13977,5,108000110,1 +13978,5,110000110,1 +13979,5,118000070,1 +13980,1,201000010,8 +13981,1,292000010,8 +13982,1,299000040,8 +13983,1,299000070,8 +13984,1,299000110,8 +13985,1,299000120,8 +13986,1,299000140,8 +13987,2,202000010,8 +13988,2,290000010,8 +13989,2,299000010,8 +13990,2,299000150,8 +13991,2,299000190,8 +13992,2,299000200,8 +13993,2,299000210,8 +13994,3,298000050,8 +13995,3,298000060,8 +13996,3,299000060,8 +13997,3,299000170,8 +13998,3,290000120,8 +13999,3,291000050,8 +14000,3,292000020,8 +14001,4,299000670,8 +14002,4,299000680,8 +14003,4,204000010,8 +14004,4,209000040,8 +14005,4,202000070,8 +14006,4,209000070,8 +14007,4,203000110,8 +14008,4,290000110,8 +14009,5,297000100,8 +14010,5,291000020,8 +14011,5,297000130,8 +14012,5,297000140,8 +14013,5,203000010,8 +14014,5,206000030,8 +14015,5,203000050,8 +14016,5,202000090,8 +14017,5,204000080,8 +14018,5,202000150,8 +14019,3,170004,3 +14020,3,180004,3 +14021,4,140000,3 +14022,5,150010,3 +14023,5,150020,3 +14024,5,150030,3 +14025,5,150040,3 +14027,3,101000050,1 +14028,3,101000060,1 +14029,3,101000080,1 +14030,3,101000040,1 +14031,3,109000060,1 +14032,3,109000070,1 +14033,3,109000080,1 +14034,3,105000060,1 +14035,3,105000070,1 +14036,3,105000080,1 +14037,3,104000050,1 +14038,3,106000050,1 +14039,3,102000060,1 +14040,3,102000070,1 +14041,3,102000080,1 +14042,3,103000050,1 +14043,3,105000050,1 +14044,3,107000060,1 +14045,3,107000070,1 +14046,3,107000080,1 +14047,3,108000050,1 +14048,3,109000050,1 +14049,3,103000060,1 +14050,3,103000070,1 +14051,3,103000080,1 +14052,3,110000050,1 +14053,3,106000060,1 +14054,3,106000070,1 +14055,3,106000080,1 +14056,3,101000070,1 +14057,3,110000060,1 +14058,3,104000060,1 +14059,3,104000070,1 +14060,3,104000080,1 +14061,3,102000050,1 +14062,3,104000170,1 +14063,3,104000260,1 +14064,3,111000010,1 +14065,3,111000020,1 +14066,3,111000030,1 +14067,3,112000020,1 +14068,3,112000030,1 +14069,3,108000060,1 +14070,3,108000070,1 +14071,3,108000080,1 +14072,3,107000050,1 +14073,3,112000010,1 +14074,3,110000070,1 +14075,3,110000080,1 +14076,3,118000020,1 +14077,3,118000030,1 +14078,3,118000040,1 +14079,4,101000090,1 +14080,4,101000100,1 +14081,4,101000110,1 +14082,4,109000100,1 +14083,4,105000100,1 +14084,4,105000110,1 +14085,4,108000090,1 +14086,4,110000090,1 +14087,4,102000100,1 +14088,4,102000110,1 +14089,4,106000090,1 +14090,4,109000090,1 +14091,4,107000100,1 +14092,4,103000090,1 +14093,4,102000090,1 +14094,4,103000100,1 +14095,4,106000100,1 +14096,4,106000110,1 +14097,4,104000090,1 +14098,4,104000100,1 +14099,4,104000110,1 +14100,4,107000090,1 +14101,4,104000180,1 +14102,4,111000040,1 +14103,4,112000040,1 +14104,4,108000100,1 +14105,4,105000090,1 +14106,4,110000100,1 +14107,4,118000050,1 +14108,4,118000060,1 +14109,5,101000120,1 +14110,5,109000110,1 +14111,5,105000120,1 +14112,5,102000120,1 +14113,5,107000110,1 +14114,5,103000120,1 +14115,5,106000120,1 +14116,5,104000120,1 +14117,5,104000190,1 +14118,5,111000060,1 +14119,5,112000060,1 +14120,5,108000110,1 +14121,5,110000110,1 +14122,5,118000070,1 +14123,1,201000010,8 +14124,1,292000010,8 +14125,1,299000040,8 +14126,1,299000070,8 +14127,1,299000110,8 +14128,1,299000120,8 +14129,1,299000140,8 +14130,2,202000010,8 +14131,2,290000010,8 +14132,2,299000010,8 +14133,2,299000150,8 +14134,2,299000190,8 +14135,2,299000200,8 +14136,2,299000210,8 +14137,3,298000050,8 +14138,3,298000060,8 +14139,3,299000060,8 +14140,3,299000170,8 +14141,3,290000120,8 +14142,3,291000050,8 +14143,3,292000020,8 +14144,4,299000670,8 +14145,4,299000680,8 +14146,4,204000010,8 +14147,4,209000040,8 +14148,4,202000070,8 +14149,4,209000070,8 +14150,4,203000110,8 +14151,4,290000110,8 +14152,5,297000100,8 +14153,5,291000020,8 +14154,5,297000130,8 +14155,5,297000140,8 +14156,5,203000010,8 +14157,5,206000030,8 +14158,5,203000050,8 +14159,5,202000090,8 +14160,5,204000080,8 +14161,5,202000150,8 +14162,3,170004,3 +14163,3,180004,3 +14164,4,140000,3 +14165,5,150010,3 +14166,5,150020,3 +14167,5,150030,3 +14168,5,150040,3 +14170,3,101000050,1 +14171,3,101000060,1 +14172,3,101000080,1 +14173,3,101000040,1 +14174,3,109000060,1 +14175,3,109000070,1 +14176,3,109000080,1 +14177,3,105000060,1 +14178,3,105000070,1 +14179,3,105000080,1 +14180,3,104000050,1 +14181,3,106000050,1 +14182,3,102000060,1 +14183,3,102000070,1 +14184,3,102000080,1 +14185,3,103000050,1 +14186,3,105000050,1 +14187,3,107000060,1 +14188,3,107000070,1 +14189,3,107000080,1 +14190,3,108000050,1 +14191,3,109000050,1 +14192,3,103000060,1 +14193,3,103000070,1 +14194,3,103000080,1 +14195,3,110000050,1 +14196,3,106000060,1 +14197,3,106000070,1 +14198,3,106000080,1 +14199,3,101000070,1 +14200,3,110000060,1 +14201,3,104000060,1 +14202,3,104000070,1 +14203,3,104000080,1 +14204,3,102000050,1 +14205,3,104000170,1 +14206,3,104000260,1 +14207,3,111000010,1 +14208,3,111000020,1 +14209,3,111000030,1 +14210,3,112000020,1 +14211,3,112000030,1 +14212,3,108000060,1 +14213,3,108000070,1 +14214,3,108000080,1 +14215,3,107000050,1 +14216,3,112000010,1 +14217,3,110000070,1 +14218,3,110000080,1 +14219,3,118000020,1 +14220,3,118000030,1 +14221,3,118000040,1 +14222,4,101000090,1 +14223,4,101000100,1 +14224,4,101000110,1 +14225,4,109000100,1 +14226,4,105000100,1 +14227,4,105000110,1 +14228,4,108000090,1 +14229,4,110000090,1 +14230,4,102000100,1 +14231,4,102000110,1 +14232,4,106000090,1 +14233,4,109000090,1 +14234,4,107000100,1 +14235,4,103000090,1 +14236,4,102000090,1 +14237,4,103000100,1 +14238,4,106000100,1 +14239,4,106000110,1 +14240,4,104000090,1 +14241,4,104000100,1 +14242,4,104000110,1 +14243,4,107000090,1 +14244,4,104000180,1 +14245,4,111000040,1 +14246,4,112000040,1 +14247,4,108000100,1 +14248,4,105000090,1 +14249,4,110000100,1 +14250,4,118000050,1 +14251,4,118000060,1 +14252,5,101000120,1 +14253,5,109000110,1 +14254,5,105000120,1 +14255,5,102000120,1 +14256,5,107000110,1 +14257,5,103000120,1 +14258,5,106000120,1 +14259,5,104000120,1 +14260,5,104000190,1 +14261,5,111000060,1 +14262,5,112000060,1 +14263,5,108000110,1 +14264,5,110000110,1 +14265,5,118000070,1 +14266,1,201000010,8 +14267,1,292000010,8 +14268,1,299000040,8 +14269,1,299000070,8 +14270,1,299000110,8 +14271,1,299000120,8 +14272,1,299000140,8 +14273,2,202000010,8 +14274,2,290000010,8 +14275,2,299000010,8 +14276,2,299000150,8 +14277,2,299000190,8 +14278,2,299000200,8 +14279,2,299000210,8 +14280,3,298000050,8 +14281,3,298000060,8 +14282,3,299000060,8 +14283,3,299000170,8 +14284,3,290000120,8 +14285,3,291000050,8 +14286,3,292000020,8 +14287,4,299000670,8 +14288,4,299000680,8 +14289,4,204000010,8 +14290,4,209000040,8 +14291,4,202000070,8 +14292,4,209000070,8 +14293,4,203000110,8 +14294,4,290000110,8 +14295,5,297000100,8 +14296,5,291000020,8 +14297,5,297000130,8 +14298,5,297000140,8 +14299,5,203000010,8 +14300,5,206000030,8 +14301,5,203000050,8 +14302,5,202000090,8 +14303,5,204000080,8 +14304,5,202000150,8 +14305,3,170004,3 +14306,3,180004,3 +14307,4,140000,3 +14308,5,150010,3 +14309,5,150020,3 +14310,5,150030,3 +14311,5,150040,3 +14312,5,190000,3 +14313,5,200000,3 +14314,5,210000,3 +14316,1,101000010,1 +14317,1,102000010,1 +14318,1,103000010,1 +14319,1,104000010,1 +14320,1,105000010,1 +14321,1,106000010,1 +14322,1,107000010,1 +14323,1,108000010,1 +14324,1,109000010,1 +14325,1,110000010,1 +14326,2,101000020,1 +14327,2,101000030,1 +14328,2,102000020,1 +14329,2,102000030,1 +14330,2,102000040,1 +14331,2,103000020,1 +14332,2,103000030,1 +14333,2,103000040,1 +14334,2,104000020,1 +14335,2,104000030,1 +14336,2,104000040,1 +14337,2,105000020,1 +14338,2,105000030,1 +14339,2,105000040,1 +14340,2,106000020,1 +14341,2,106000030,1 +14342,2,106000040,1 +14343,2,107000020,1 +14344,2,107000030,1 +14345,2,107000040,1 +14346,2,108000020,1 +14347,2,108000030,1 +14348,2,108000040,1 +14349,2,109000020,1 +14350,2,109000030,1 +14351,2,109000040,1 +14352,2,110000020,1 +14353,2,110000030,1 +14354,2,110000040,1 +14355,2,118000010,1 +14356,3,101000050,1 +14357,3,101000060,1 +14358,3,101000080,1 +14359,3,101000040,1 +14360,3,109000060,1 +14361,3,109000070,1 +14362,3,109000080,1 +14363,3,105000060,1 +14364,3,105000070,1 +14365,3,105000080,1 +14366,3,104000050,1 +14367,3,106000050,1 +14368,3,102000060,1 +14369,3,102000070,1 +14370,3,102000080,1 +14371,3,103000050,1 +14372,3,105000050,1 +14373,3,107000060,1 +14374,3,107000070,1 +14375,3,107000080,1 +14376,3,108000050,1 +14377,3,109000050,1 +14378,3,103000060,1 +14379,3,103000070,1 +14380,3,103000080,1 +14381,3,110000050,1 +14382,3,106000060,1 +14383,3,106000070,1 +14384,3,106000080,1 +14385,3,101000070,1 +14386,3,110000060,1 +14387,3,104000060,1 +14388,3,104000070,1 +14389,3,104000080,1 +14390,3,102000050,1 +14391,3,104000170,1 +14392,3,104000260,1 +14393,3,111000010,1 +14394,3,111000020,1 +14395,3,111000030,1 +14396,3,112000020,1 +14397,3,112000030,1 +14398,3,108000060,1 +14399,3,108000070,1 +14400,3,108000080,1 +14401,3,107000050,1 +14402,3,112000010,1 +14403,3,110000070,1 +14404,3,110000080,1 +14405,3,118000020,1 +14406,3,118000030,1 +14407,3,118000040,1 +14408,4,101000090,1 +14409,4,101000100,1 +14410,4,101000110,1 +14411,4,109000100,1 +14412,4,105000100,1 +14413,4,105000110,1 +14414,4,108000090,1 +14415,4,110000090,1 +14416,4,102000100,1 +14417,4,102000110,1 +14418,4,106000090,1 +14419,4,109000090,1 +14420,4,107000100,1 +14421,4,103000090,1 +14422,4,102000090,1 +14423,4,103000100,1 +14424,4,106000100,1 +14425,4,106000110,1 +14426,4,104000090,1 +14427,4,104000100,1 +14428,4,104000110,1 +14429,4,107000090,1 +14430,4,104000180,1 +14431,4,111000040,1 +14432,4,112000040,1 +14433,4,108000100,1 +14434,4,105000090,1 +14435,4,110000100,1 +14436,4,118000050,1 +14437,4,118000060,1 +14438,5,101000120,1 +14439,5,109000110,1 +14440,5,105000120,1 +14441,5,102000120,1 +14442,5,107000110,1 +14443,5,103000120,1 +14444,5,106000120,1 +14445,5,104000120,1 +14446,5,104000190,1 +14447,5,111000060,1 +14448,5,112000060,1 +14449,5,108000110,1 +14450,5,110000110,1 +14451,5,118000070,1 +14452,1,201000010,8 +14453,1,292000010,8 +14454,1,299000040,8 +14455,1,299000070,8 +14456,1,299000110,8 +14457,1,299000120,8 +14458,1,299000140,8 +14459,2,202000010,8 +14460,2,290000010,8 +14461,2,299000010,8 +14462,2,299000150,8 +14463,2,299000190,8 +14464,2,299000200,8 +14465,2,299000210,8 +14466,3,298000050,8 +14467,3,298000060,8 +14468,3,299000060,8 +14469,3,299000170,8 +14470,3,290000120,8 +14471,3,291000050,8 +14472,3,292000020,8 +14473,4,299000670,8 +14474,4,299000680,8 +14475,4,204000010,8 +14476,4,209000040,8 +14477,4,202000070,8 +14478,4,209000070,8 +14479,4,203000110,8 +14480,4,290000110,8 +14481,4,206000110,8 +14482,5,297000100,8 +14483,5,291000020,8 +14484,5,297000130,8 +14485,5,297000140,8 +14486,5,203000010,8 +14487,5,206000030,8 +14488,5,203000050,8 +14489,5,202000090,8 +14490,5,204000080,8 +14491,5,202000150,8 +14492,5,204000100,8 +14493,1,170002,3 +14494,1,180002,3 +14495,2,170003,3 +14496,2,180003,3 +14497,3,170004,3 +14498,3,180004,3 +14499,4,140000,3 +14500,5,150010,3 +14501,5,150020,3 +14502,5,150030,3 +14503,5,150040,3 +14505,1,101000010,1 +14506,1,102000010,1 +14507,1,103000010,1 +14508,1,104000010,1 +14509,1,105000010,1 +14510,1,106000010,1 +14511,1,107000010,1 +14512,1,108000010,1 +14513,1,109000010,1 +14514,1,110000010,1 +14515,2,101000020,1 +14516,2,101000030,1 +14517,2,102000020,1 +14518,2,102000030,1 +14519,2,102000040,1 +14520,2,103000020,1 +14521,2,103000030,1 +14522,2,103000040,1 +14523,2,104000020,1 +14524,2,104000030,1 +14525,2,104000040,1 +14526,2,105000020,1 +14527,2,105000030,1 +14528,2,105000040,1 +14529,2,106000020,1 +14530,2,106000030,1 +14531,2,106000040,1 +14532,2,107000020,1 +14533,2,107000030,1 +14534,2,107000040,1 +14535,2,108000020,1 +14536,2,108000030,1 +14537,2,108000040,1 +14538,2,109000020,1 +14539,2,109000030,1 +14540,2,109000040,1 +14541,2,110000020,1 +14542,2,110000030,1 +14543,2,110000040,1 +14544,2,118000010,1 +14545,3,101000050,1 +14546,3,101000060,1 +14547,3,101000080,1 +14548,3,101000040,1 +14549,3,109000060,1 +14550,3,109000070,1 +14551,3,109000080,1 +14552,3,105000060,1 +14553,3,105000070,1 +14554,3,105000080,1 +14555,3,104000050,1 +14556,3,106000050,1 +14557,3,102000060,1 +14558,3,102000070,1 +14559,3,102000080,1 +14560,3,103000050,1 +14561,3,105000050,1 +14562,3,107000060,1 +14563,3,107000070,1 +14564,3,107000080,1 +14565,3,108000050,1 +14566,3,109000050,1 +14567,3,103000060,1 +14568,3,103000070,1 +14569,3,103000080,1 +14570,3,110000050,1 +14571,3,106000060,1 +14572,3,106000070,1 +14573,3,106000080,1 +14574,3,101000070,1 +14575,3,110000060,1 +14576,3,104000060,1 +14577,3,104000070,1 +14578,3,104000080,1 +14579,3,102000050,1 +14580,3,104000170,1 +14581,3,104000260,1 +14582,3,111000010,1 +14583,3,111000020,1 +14584,3,111000030,1 +14585,3,112000020,1 +14586,3,112000030,1 +14587,3,108000060,1 +14588,3,108000070,1 +14589,3,108000080,1 +14590,3,107000050,1 +14591,3,112000010,1 +14592,3,110000070,1 +14593,3,110000080,1 +14594,3,118000020,1 +14595,3,118000030,1 +14596,3,118000040,1 +14597,4,101000090,1 +14598,4,101000100,1 +14599,4,101000110,1 +14600,4,109000100,1 +14601,4,105000100,1 +14602,4,105000110,1 +14603,4,108000090,1 +14604,4,110000090,1 +14605,4,102000100,1 +14606,4,102000110,1 +14607,4,106000090,1 +14608,4,109000090,1 +14609,4,107000100,1 +14610,4,103000090,1 +14611,4,102000090,1 +14612,4,103000100,1 +14613,4,106000100,1 +14614,4,106000110,1 +14615,4,104000090,1 +14616,4,104000100,1 +14617,4,104000110,1 +14618,4,107000090,1 +14619,4,104000180,1 +14620,4,111000040,1 +14621,4,112000040,1 +14622,4,108000100,1 +14623,4,105000090,1 +14624,4,110000100,1 +14625,4,118000050,1 +14626,4,118000060,1 +14627,5,101000120,1 +14628,5,109000110,1 +14629,5,105000120,1 +14630,5,102000120,1 +14631,5,107000110,1 +14632,5,103000120,1 +14633,5,106000120,1 +14634,5,104000120,1 +14635,5,104000190,1 +14636,5,111000060,1 +14637,5,112000060,1 +14638,5,108000110,1 +14639,5,110000110,1 +14640,5,118000070,1 +14641,1,201000010,8 +14642,1,292000010,8 +14643,1,299000040,8 +14644,1,299000070,8 +14645,1,299000110,8 +14646,1,299000120,8 +14647,1,299000140,8 +14648,2,202000010,8 +14649,2,290000010,8 +14650,2,299000010,8 +14651,2,299000150,8 +14652,2,299000190,8 +14653,2,299000200,8 +14654,2,299000210,8 +14655,3,298000050,8 +14656,3,298000060,8 +14657,3,299000060,8 +14658,3,299000170,8 +14659,3,290000120,8 +14660,3,291000050,8 +14661,3,292000020,8 +14662,4,299000670,8 +14663,4,299000680,8 +14664,4,204000010,8 +14665,4,209000040,8 +14666,4,202000070,8 +14667,4,209000070,8 +14668,4,203000110,8 +14669,4,290000110,8 +14670,4,206000110,8 +14671,5,297000100,8 +14672,5,291000020,8 +14673,5,297000130,8 +14674,5,297000140,8 +14675,5,203000010,8 +14676,5,206000030,8 +14677,5,203000050,8 +14678,5,202000090,8 +14679,5,204000080,8 +14680,5,202000150,8 +14681,5,204000100,8 +14682,2,170003,3 +14683,2,180003,3 +14684,3,170004,3 +14685,3,180004,3 +14686,4,140000,3 +14687,5,150010,3 +14688,5,150020,3 +14689,5,150030,3 +14690,5,150040,3 +14692,3,101000050,1 +14693,3,101000060,1 +14694,3,101000080,1 +14695,3,101000040,1 +14696,3,109000060,1 +14697,3,109000070,1 +14698,3,109000080,1 +14699,3,105000060,1 +14700,3,105000070,1 +14701,3,105000080,1 +14702,3,104000050,1 +14703,3,106000050,1 +14704,3,102000060,1 +14705,3,102000070,1 +14706,3,102000080,1 +14707,3,103000050,1 +14708,3,105000050,1 +14709,3,107000060,1 +14710,3,107000070,1 +14711,3,107000080,1 +14712,3,108000050,1 +14713,3,109000050,1 +14714,3,103000060,1 +14715,3,103000070,1 +14716,3,103000080,1 +14717,3,110000050,1 +14718,3,106000060,1 +14719,3,106000070,1 +14720,3,106000080,1 +14721,3,101000070,1 +14722,3,110000060,1 +14723,3,104000060,1 +14724,3,104000070,1 +14725,3,104000080,1 +14726,3,102000050,1 +14727,3,104000170,1 +14728,3,104000260,1 +14729,3,111000010,1 +14730,3,111000020,1 +14731,3,111000030,1 +14732,3,112000020,1 +14733,3,112000030,1 +14734,3,108000060,1 +14735,3,108000070,1 +14736,3,108000080,1 +14737,3,107000050,1 +14738,3,112000010,1 +14739,3,110000070,1 +14740,3,110000080,1 +14741,3,118000020,1 +14742,3,118000030,1 +14743,3,118000040,1 +14744,4,101000090,1 +14745,4,101000100,1 +14746,4,101000110,1 +14747,4,109000100,1 +14748,4,105000100,1 +14749,4,105000110,1 +14750,4,108000090,1 +14751,4,110000090,1 +14752,4,102000100,1 +14753,4,102000110,1 +14754,4,106000090,1 +14755,4,109000090,1 +14756,4,107000100,1 +14757,4,103000090,1 +14758,4,102000090,1 +14759,4,103000100,1 +14760,4,106000100,1 +14761,4,106000110,1 +14762,4,104000090,1 +14763,4,104000100,1 +14764,4,104000110,1 +14765,4,107000090,1 +14766,4,104000180,1 +14767,4,111000040,1 +14768,4,112000040,1 +14769,4,108000100,1 +14770,4,105000090,1 +14771,4,110000100,1 +14772,4,118000050,1 +14773,4,118000060,1 +14774,5,101000120,1 +14775,5,109000110,1 +14776,5,105000120,1 +14777,5,102000120,1 +14778,5,107000110,1 +14779,5,103000120,1 +14780,5,106000120,1 +14781,5,104000120,1 +14782,5,104000190,1 +14783,5,111000060,1 +14784,5,112000060,1 +14785,5,108000110,1 +14786,5,110000110,1 +14787,5,118000070,1 +14788,1,201000010,8 +14789,1,292000010,8 +14790,1,299000040,8 +14791,1,299000070,8 +14792,1,299000110,8 +14793,1,299000120,8 +14794,1,299000140,8 +14795,2,202000010,8 +14796,2,290000010,8 +14797,2,299000010,8 +14798,2,299000150,8 +14799,2,299000190,8 +14800,2,299000200,8 +14801,2,299000210,8 +14802,3,298000050,8 +14803,3,298000060,8 +14804,3,299000060,8 +14805,3,299000170,8 +14806,3,290000120,8 +14807,3,291000050,8 +14808,3,292000020,8 +14809,4,299000670,8 +14810,4,299000680,8 +14811,4,204000010,8 +14812,4,209000040,8 +14813,4,202000070,8 +14814,4,209000070,8 +14815,4,203000110,8 +14816,4,290000110,8 +14817,4,206000110,8 +14818,5,297000100,8 +14819,5,291000020,8 +14820,5,297000130,8 +14821,5,297000140,8 +14822,5,203000010,8 +14823,5,206000030,8 +14824,5,203000050,8 +14825,5,202000090,8 +14826,5,204000080,8 +14827,5,202000150,8 +14828,5,204000100,8 +14829,3,170004,3 +14830,3,180004,3 +14831,4,140000,3 +14832,5,150010,3 +14833,5,150020,3 +14834,5,150030,3 +14835,5,150040,3 +14837,3,101000050,1 +14838,3,101000060,1 +14839,3,101000080,1 +14840,3,101000040,1 +14841,3,109000060,1 +14842,3,109000070,1 +14843,3,109000080,1 +14844,3,105000060,1 +14845,3,105000070,1 +14846,3,105000080,1 +14847,3,104000050,1 +14848,3,106000050,1 +14849,3,102000060,1 +14850,3,102000070,1 +14851,3,102000080,1 +14852,3,103000050,1 +14853,3,105000050,1 +14854,3,107000060,1 +14855,3,107000070,1 +14856,3,107000080,1 +14857,3,108000050,1 +14858,3,109000050,1 +14859,3,103000060,1 +14860,3,103000070,1 +14861,3,103000080,1 +14862,3,110000050,1 +14863,3,106000060,1 +14864,3,106000070,1 +14865,3,106000080,1 +14866,3,101000070,1 +14867,3,110000060,1 +14868,3,104000060,1 +14869,3,104000070,1 +14870,3,104000080,1 +14871,3,102000050,1 +14872,3,104000170,1 +14873,3,104000260,1 +14874,3,111000010,1 +14875,3,111000020,1 +14876,3,111000030,1 +14877,3,112000020,1 +14878,3,112000030,1 +14879,3,108000060,1 +14880,3,108000070,1 +14881,3,108000080,1 +14882,3,107000050,1 +14883,3,112000010,1 +14884,3,110000070,1 +14885,3,110000080,1 +14886,3,118000020,1 +14887,3,118000030,1 +14888,3,118000040,1 +14889,4,101000090,1 +14890,4,101000100,1 +14891,4,101000110,1 +14892,4,109000100,1 +14893,4,105000100,1 +14894,4,105000110,1 +14895,4,108000090,1 +14896,4,110000090,1 +14897,4,102000100,1 +14898,4,102000110,1 +14899,4,106000090,1 +14900,4,109000090,1 +14901,4,107000100,1 +14902,4,103000090,1 +14903,4,102000090,1 +14904,4,103000100,1 +14905,4,106000100,1 +14906,4,106000110,1 +14907,4,104000090,1 +14908,4,104000100,1 +14909,4,104000110,1 +14910,4,107000090,1 +14911,4,104000180,1 +14912,4,111000040,1 +14913,4,112000040,1 +14914,4,108000100,1 +14915,4,105000090,1 +14916,4,110000100,1 +14917,4,118000050,1 +14918,4,118000060,1 +14919,5,101000120,1 +14920,5,109000110,1 +14921,5,105000120,1 +14922,5,102000120,1 +14923,5,107000110,1 +14924,5,103000120,1 +14925,5,106000120,1 +14926,5,104000120,1 +14927,5,104000190,1 +14928,5,111000060,1 +14929,5,112000060,1 +14930,5,108000110,1 +14931,5,110000110,1 +14932,5,118000070,1 +14933,1,201000010,8 +14934,1,292000010,8 +14935,1,299000040,8 +14936,1,299000070,8 +14937,1,299000110,8 +14938,1,299000120,8 +14939,1,299000140,8 +14940,2,202000010,8 +14941,2,290000010,8 +14942,2,299000010,8 +14943,2,299000150,8 +14944,2,299000190,8 +14945,2,299000200,8 +14946,2,299000210,8 +14947,3,298000050,8 +14948,3,298000060,8 +14949,3,299000060,8 +14950,3,299000170,8 +14951,3,290000120,8 +14952,3,291000050,8 +14953,3,292000020,8 +14954,4,299000670,8 +14955,4,299000680,8 +14956,4,204000010,8 +14957,4,209000040,8 +14958,4,202000070,8 +14959,4,209000070,8 +14960,4,203000110,8 +14961,4,290000110,8 +14962,4,206000110,8 +14963,5,297000100,8 +14964,5,291000020,8 +14965,5,297000130,8 +14966,5,297000140,8 +14967,5,203000010,8 +14968,5,206000030,8 +14969,5,203000050,8 +14970,5,202000090,8 +14971,5,204000080,8 +14972,5,202000150,8 +14973,5,204000100,8 +14974,3,170004,3 +14975,3,180004,3 +14976,4,140000,3 +14977,5,150010,3 +14978,5,150020,3 +14979,5,150030,3 +14980,5,150040,3 +14982,3,101000050,1 +14983,3,101000060,1 +14984,3,101000080,1 +14985,3,101000040,1 +14986,3,109000060,1 +14987,3,109000070,1 +14988,3,109000080,1 +14989,3,105000060,1 +14990,3,105000070,1 +14991,3,105000080,1 +14992,3,104000050,1 +14993,3,106000050,1 +14994,3,102000060,1 +14995,3,102000070,1 +14996,3,102000080,1 +14997,3,103000050,1 +14998,3,105000050,1 +14999,3,107000060,1 +15000,3,107000070,1 +15001,3,107000080,1 +15002,3,108000050,1 +15003,3,109000050,1 +15004,3,103000060,1 +15005,3,103000070,1 +15006,3,103000080,1 +15007,3,110000050,1 +15008,3,106000060,1 +15009,3,106000070,1 +15010,3,106000080,1 +15011,3,101000070,1 +15012,3,110000060,1 +15013,3,104000060,1 +15014,3,104000070,1 +15015,3,104000080,1 +15016,3,102000050,1 +15017,3,104000170,1 +15018,3,104000260,1 +15019,3,111000010,1 +15020,3,111000020,1 +15021,3,111000030,1 +15022,3,112000020,1 +15023,3,112000030,1 +15024,3,108000060,1 +15025,3,108000070,1 +15026,3,108000080,1 +15027,3,107000050,1 +15028,3,112000010,1 +15029,3,110000070,1 +15030,3,110000080,1 +15031,3,118000020,1 +15032,3,118000030,1 +15033,3,118000040,1 +15034,4,101000090,1 +15035,4,101000100,1 +15036,4,101000110,1 +15037,4,109000100,1 +15038,4,105000100,1 +15039,4,105000110,1 +15040,4,108000090,1 +15041,4,110000090,1 +15042,4,102000100,1 +15043,4,102000110,1 +15044,4,106000090,1 +15045,4,109000090,1 +15046,4,107000100,1 +15047,4,103000090,1 +15048,4,102000090,1 +15049,4,103000100,1 +15050,4,106000100,1 +15051,4,106000110,1 +15052,4,104000090,1 +15053,4,104000100,1 +15054,4,104000110,1 +15055,4,107000090,1 +15056,4,104000180,1 +15057,4,111000040,1 +15058,4,112000040,1 +15059,4,108000100,1 +15060,4,105000090,1 +15061,4,110000100,1 +15062,4,118000050,1 +15063,4,118000060,1 +15064,5,101000120,1 +15065,5,109000110,1 +15066,5,105000120,1 +15067,5,102000120,1 +15068,5,107000110,1 +15069,5,103000120,1 +15070,5,106000120,1 +15071,5,104000120,1 +15072,5,104000190,1 +15073,5,111000060,1 +15074,5,112000060,1 +15075,5,108000110,1 +15076,5,110000110,1 +15077,5,118000070,1 +15078,1,201000010,8 +15079,1,292000010,8 +15080,1,299000040,8 +15081,1,299000070,8 +15082,1,299000110,8 +15083,1,299000120,8 +15084,1,299000140,8 +15085,2,202000010,8 +15086,2,290000010,8 +15087,2,299000010,8 +15088,2,299000150,8 +15089,2,299000190,8 +15090,2,299000200,8 +15091,2,299000210,8 +15092,3,298000050,8 +15093,3,298000060,8 +15094,3,299000060,8 +15095,3,299000170,8 +15096,3,290000120,8 +15097,3,291000050,8 +15098,3,292000020,8 +15099,4,299000670,8 +15100,4,299000680,8 +15101,4,204000010,8 +15102,4,209000040,8 +15103,4,202000070,8 +15104,4,209000070,8 +15105,4,203000110,8 +15106,4,290000110,8 +15107,4,206000110,8 +15108,5,297000100,8 +15109,5,291000020,8 +15110,5,297000130,8 +15111,5,297000140,8 +15112,5,203000010,8 +15113,5,206000030,8 +15114,5,203000050,8 +15115,5,202000090,8 +15116,5,204000080,8 +15117,5,202000150,8 +15118,5,204000100,8 +15119,3,170004,3 +15120,3,180004,3 +15121,4,140000,3 +15122,5,150010,3 +15123,5,150020,3 +15124,5,150030,3 +15125,5,150040,3 +15126,5,190000,3 +15127,5,200000,3 +15128,5,210000,3 +15130,3,111000010,1 +15131,3,111000020,1 +15132,3,111000030,1 +15133,3,112000020,1 +15134,3,112000030,1 +15135,3,108000060,1 +15136,3,108000070,1 +15137,3,108000080,1 +15138,3,107000050,1 +15139,3,112000010,1 +15140,3,110000070,1 +15141,3,110000080,1 +15142,4,111000040,1 +15143,4,112000040,1 +15144,4,108000100,1 +15145,4,105000090,1 +15146,4,110000100,1 +15147,5,111000060,1 +15148,5,112000060,1 +15149,5,108000110,1 +15150,5,110000110,1 +15151,1,108000000,2 +15152,2,108000001,2 +15153,3,108000002,2 +15154,4,108000003,2 +15155,5,108000004,2 +15156,1,107000000,2 +15157,2,107000001,2 +15158,3,107000002,2 +15159,4,107000003,2 +15160,5,107000004,2 +15161,1,120000000,2 +15162,2,120000001,2 +15163,3,120000002,2 +15164,4,120000003,2 +15165,5,120000004,2 +15166,3,170004,3 +15167,4,170005,3 +15168,3,180004,3 +15169,4,180005,3 +15170,4,140000,3 +15171,4,150010,3 +15172,4,150020,3 +15173,4,150030,3 +15174,4,150040,3 +15176,3,101000050,1 +15177,3,101000060,1 +15178,3,101000080,1 +15179,3,101000040,1 +15180,3,109000060,1 +15181,3,109000070,1 +15182,3,109000080,1 +15183,3,105000060,1 +15184,3,105000070,1 +15185,3,105000080,1 +15186,3,104000050,1 +15187,3,106000050,1 +15188,4,101000090,1 +15189,4,101000100,1 +15190,4,101000110,1 +15191,4,109000100,1 +15192,4,105000100,1 +15193,4,105000110,1 +15194,4,108000090,1 +15195,4,110000090,1 +15196,5,101000120,1 +15197,5,109000110,1 +15198,5,105000120,1 +15199,1,101000000,2 +15200,2,101000001,2 +15201,3,101000002,2 +15202,4,101000008,2 +15203,5,101000004,2 +15204,1,109000000,2 +15205,2,109000001,2 +15206,3,109000002,2 +15207,4,109000003,2 +15208,5,109000004,2 +15209,4,110024,3 +15210,4,110034,3 +15211,4,110044,3 +15212,4,110054,3 +15213,3,110060,3 +15214,3,110070,3 +15215,3,110080,3 +15216,3,110090,3 +15217,3,110100,3 +15218,3,110110,3 +15219,3,110120,3 +15220,3,110130,3 +15221,3,110140,3 +15222,3,110150,3 +15223,3,110160,3 +15224,3,110170,3 +15225,3,110180,3 +15226,3,110190,3 +15227,3,110200,3 +15228,3,110210,3 +15229,3,110220,3 +15230,3,110230,3 +15231,3,110240,3 +15232,3,110250,3 +15233,3,110260,3 +15234,3,110270,3 +15235,3,110620,3 +15236,3,110670,3 +15237,4,140000,3 +15238,4,150010,3 +15239,4,150020,3 +15240,4,150030,3 +15241,4,150040,3 +15243,3,102000060,1 +15244,3,102000070,1 +15245,3,102000080,1 +15246,3,103000050,1 +15247,3,105000050,1 +15248,3,107000060,1 +15249,3,107000070,1 +15250,3,107000080,1 +15251,3,108000050,1 +15252,3,109000050,1 +15253,3,103000060,1 +15254,3,103000070,1 +15255,3,103000080,1 +15256,3,110000050,1 +15257,4,102000100,1 +15258,4,102000110,1 +15259,4,102000350,1 +15260,4,106000090,1 +15261,4,109000090,1 +15262,4,107000100,1 +15263,4,103000090,1 +15264,4,102000090,1 +15265,4,103000100,1 +15266,5,102000120,1 +15267,5,107000110,1 +15268,5,103000120,1 +15269,1,102000000,2 +15270,2,102000001,2 +15271,3,102000002,2 +15272,4,102000003,2 +15273,5,102000004,2 +15274,1,105000000,2 +15275,2,105000001,2 +15276,3,105000002,2 +15277,4,105000003,2 +15278,5,105000004,2 +15279,1,112000000,2 +15280,2,112000001,2 +15281,3,112000002,2 +15282,4,112000003,2 +15283,5,112000004,2 +15284,4,120024,3 +15285,4,120034,3 +15286,4,120044,3 +15287,4,120054,3 +15288,3,120241,3 +15289,3,120251,3 +15290,3,120261,3 +15291,3,120271,3 +15292,3,120300,3 +15293,3,120310,3 +15294,3,120320,3 +15295,3,120330,3 +15296,3,120340,3 +15297,3,120350,3 +15298,3,120360,3 +15299,3,120370,3 +15300,3,120380,3 +15301,3,120390,3 +15302,3,120400,3 +15303,3,120410,3 +15304,3,120420,3 +15305,3,120430,3 +15306,3,120450,3 +15307,3,120460,3 +15308,3,120550,3 +15309,3,120560,3 +15310,3,120570,3 +15311,3,120990,3 +15312,3,121000,3 +15313,3,121010,3 +15314,3,121020,3 +15315,3,121100,3 +15316,4,140000,3 +15317,4,150010,3 +15318,4,150020,3 +15319,4,150030,3 +15320,4,150040,3 +15322,3,118000020,1 +15323,3,118000030,1 +15324,3,118000040,1 +15325,3,106000060,1 +15326,3,106000070,1 +15327,3,106000080,1 +15328,3,101000070,1 +15329,3,110000060,1 +15330,3,104000060,1 +15331,3,104000070,1 +15332,3,104000080,1 +15333,3,102000050,1 +15334,3,104000170,1 +15335,3,104000260,1 +15336,4,118000050,1 +15337,4,118000060,1 +15338,4,106000100,1 +15339,4,106000110,1 +15340,4,104000090,1 +15341,4,104000100,1 +15342,4,104000110,1 +15343,4,104000270,1 +15344,4,107000090,1 +15345,4,104000180,1 +15346,5,118000070,1 +15347,5,106000120,1 +15348,5,104000120,1 +15349,5,104000190,1 +15350,1,103000000,2 +15351,2,103000001,2 +15352,3,103000002,2 +15353,4,103000003,2 +15354,5,103000004,2 +15355,1,111000000,2 +15356,2,111000001,2 +15357,3,111000002,2 +15358,4,111000003,2 +15359,5,111000004,2 +15360,1,115000000,2 +15361,2,115000001,2 +15362,3,115000002,2 +15363,4,115000003,2 +15364,5,115000004,2 +15365,4,130024,3 +15366,4,130034,3 +15367,4,130044,3 +15368,4,130054,3 +15369,3,130060,3 +15370,3,130070,3 +15371,3,130080,3 +15372,3,130090,3 +15373,3,130100,3 +15374,3,130110,3 +15375,3,130120,3 +15376,3,130130,3 +15377,3,130140,3 +15378,3,130150,3 +15379,3,130160,3 +15380,3,130170,3 +15381,3,130180,3 +15382,3,130190,3 +15383,3,130200,3 +15384,3,130420,3 +15385,3,130510,3 +15386,3,130520,3 +15387,3,130531,3 +15388,3,130540,3 +15389,3,130660,3 +15390,3,130700,3 +15391,3,130790,3 +15392,3,130800,3 +15393,3,131130,3 +15394,4,140000,3 +15395,4,150010,3 +15396,4,150020,3 +15397,4,150030,3 +15398,4,150040,3 +15400,1,101000010,1 +15401,1,102000010,1 +15402,1,103000010,1 +15403,1,104000010,1 +15404,1,105000010,1 +15405,1,106000010,1 +15406,1,107000010,1 +15407,1,108000010,1 +15408,1,109000010,1 +15409,1,110000010,1 +15410,2,101000020,1 +15411,2,101000030,1 +15412,2,102000020,1 +15413,2,102000030,1 +15414,2,102000040,1 +15415,2,103000020,1 +15416,2,103000030,1 +15417,2,103000040,1 +15418,2,104000020,1 +15419,2,104000030,1 +15420,2,104000040,1 +15421,2,105000020,1 +15422,2,105000030,1 +15423,2,105000040,1 +15424,2,106000020,1 +15425,2,106000030,1 +15426,2,106000040,1 +15427,2,107000020,1 +15428,2,107000030,1 +15429,2,107000040,1 +15430,2,108000020,1 +15431,2,108000030,1 +15432,2,108000040,1 +15433,2,109000020,1 +15434,2,109000030,1 +15435,2,109000040,1 +15436,2,110000020,1 +15437,2,110000030,1 +15438,2,110000040,1 +15439,2,118000010,1 +15440,3,101000050,1 +15441,3,101000060,1 +15442,3,101000080,1 +15443,3,101000040,1 +15444,3,109000060,1 +15445,3,109000070,1 +15446,3,109000080,1 +15447,3,105000060,1 +15448,3,105000070,1 +15449,3,105000080,1 +15450,3,104000050,1 +15451,3,106000050,1 +15452,3,102000060,1 +15453,3,102000070,1 +15454,3,102000080,1 +15455,3,103000050,1 +15456,3,105000050,1 +15457,3,107000060,1 +15458,3,107000070,1 +15459,3,107000080,1 +15460,3,108000050,1 +15461,3,109000050,1 +15462,3,103000060,1 +15463,3,103000070,1 +15464,3,103000080,1 +15465,3,110000050,1 +15466,3,106000060,1 +15467,3,106000070,1 +15468,3,106000080,1 +15469,3,101000070,1 +15470,3,110000060,1 +15471,3,104000060,1 +15472,3,104000070,1 +15473,3,104000080,1 +15474,3,102000050,1 +15475,3,104000170,1 +15476,3,104000260,1 +15477,3,111000010,1 +15478,3,111000020,1 +15479,3,111000030,1 +15480,3,112000020,1 +15481,3,112000030,1 +15482,3,108000060,1 +15483,3,108000070,1 +15484,3,108000080,1 +15485,3,107000050,1 +15486,3,112000010,1 +15487,3,110000070,1 +15488,3,110000080,1 +15489,3,118000020,1 +15490,3,118000030,1 +15491,3,118000040,1 +15492,4,101000090,1 +15493,4,101000100,1 +15494,4,101000110,1 +15495,4,109000100,1 +15496,4,105000100,1 +15497,4,105000110,1 +15498,4,108000090,1 +15499,4,110000090,1 +15500,4,102000100,1 +15501,4,102000110,1 +15502,4,106000090,1 +15503,4,109000090,1 +15504,4,107000100,1 +15505,4,103000090,1 +15506,4,102000090,1 +15507,4,103000100,1 +15508,4,106000100,1 +15509,4,106000110,1 +15510,4,104000090,1 +15511,4,104000100,1 +15512,4,104000110,1 +15513,4,107000090,1 +15514,4,104000180,1 +15515,4,111000040,1 +15516,4,112000040,1 +15517,4,108000100,1 +15518,4,105000090,1 +15519,4,110000100,1 +15520,4,118000050,1 +15521,4,118000060,1 +15522,5,101000120,1 +15523,5,109000110,1 +15524,5,105000120,1 +15525,5,102000120,1 +15526,5,107000110,1 +15527,5,103000120,1 +15528,5,106000120,1 +15529,5,104000120,1 +15530,5,104000190,1 +15531,5,111000060,1 +15532,5,112000060,1 +15533,5,108000110,1 +15534,5,110000110,1 +15535,5,118000070,1 +15536,1,201000010,8 +15537,1,292000010,8 +15538,1,299000040,8 +15539,1,299000070,8 +15540,1,299000110,8 +15541,1,299000120,8 +15542,1,299000140,8 +15543,2,202000010,8 +15544,2,290000010,8 +15545,2,299000010,8 +15546,2,299000150,8 +15547,2,299000190,8 +15548,2,299000200,8 +15549,2,299000210,8 +15550,3,298000050,8 +15551,3,298000060,8 +15552,3,299000060,8 +15553,3,299000170,8 +15554,3,290000120,8 +15555,3,291000050,8 +15556,3,292000020,8 +15557,4,299000670,8 +15558,4,299000680,8 +15559,4,204000010,8 +15560,4,209000040,8 +15561,4,202000070,8 +15562,4,209000070,8 +15563,4,203000110,8 +15564,4,290000110,8 +15565,4,206000110,8 +15566,4,209000160,8 +15567,5,297000100,8 +15568,5,291000020,8 +15569,5,297000130,8 +15570,5,297000140,8 +15571,5,203000010,8 +15572,5,206000030,8 +15573,5,203000050,8 +15574,5,202000090,8 +15575,5,204000080,8 +15576,5,202000150,8 +15577,5,204000100,8 +15578,5,206000170,8 +15579,1,170002,3 +15580,1,180002,3 +15581,2,170003,3 +15582,2,180003,3 +15583,3,170004,3 +15584,3,180004,3 +15585,4,140000,3 +15586,5,150010,3 +15587,5,150020,3 +15588,5,150030,3 +15589,5,150040,3 +15591,1,101000010,1 +15592,1,102000010,1 +15593,1,103000010,1 +15594,1,104000010,1 +15595,1,105000010,1 +15596,1,106000010,1 +15597,1,107000010,1 +15598,1,108000010,1 +15599,1,109000010,1 +15600,1,110000010,1 +15601,2,101000020,1 +15602,2,101000030,1 +15603,2,102000020,1 +15604,2,102000030,1 +15605,2,102000040,1 +15606,2,103000020,1 +15607,2,103000030,1 +15608,2,103000040,1 +15609,2,104000020,1 +15610,2,104000030,1 +15611,2,104000040,1 +15612,2,105000020,1 +15613,2,105000030,1 +15614,2,105000040,1 +15615,2,106000020,1 +15616,2,106000030,1 +15617,2,106000040,1 +15618,2,107000020,1 +15619,2,107000030,1 +15620,2,107000040,1 +15621,2,108000020,1 +15622,2,108000030,1 +15623,2,108000040,1 +15624,2,109000020,1 +15625,2,109000030,1 +15626,2,109000040,1 +15627,2,110000020,1 +15628,2,110000030,1 +15629,2,110000040,1 +15630,2,118000010,1 +15631,3,101000050,1 +15632,3,101000060,1 +15633,3,101000080,1 +15634,3,101000040,1 +15635,3,109000060,1 +15636,3,109000070,1 +15637,3,109000080,1 +15638,3,105000060,1 +15639,3,105000070,1 +15640,3,105000080,1 +15641,3,104000050,1 +15642,3,106000050,1 +15643,3,102000060,1 +15644,3,102000070,1 +15645,3,102000080,1 +15646,3,103000050,1 +15647,3,105000050,1 +15648,3,107000060,1 +15649,3,107000070,1 +15650,3,107000080,1 +15651,3,108000050,1 +15652,3,109000050,1 +15653,3,103000060,1 +15654,3,103000070,1 +15655,3,103000080,1 +15656,3,110000050,1 +15657,3,106000060,1 +15658,3,106000070,1 +15659,3,106000080,1 +15660,3,101000070,1 +15661,3,110000060,1 +15662,3,104000060,1 +15663,3,104000070,1 +15664,3,104000080,1 +15665,3,102000050,1 +15666,3,104000170,1 +15667,3,104000260,1 +15668,3,111000010,1 +15669,3,111000020,1 +15670,3,111000030,1 +15671,3,112000020,1 +15672,3,112000030,1 +15673,3,108000060,1 +15674,3,108000070,1 +15675,3,108000080,1 +15676,3,107000050,1 +15677,3,112000010,1 +15678,3,110000070,1 +15679,3,110000080,1 +15680,3,118000020,1 +15681,3,118000030,1 +15682,3,118000040,1 +15683,4,101000090,1 +15684,4,101000100,1 +15685,4,101000110,1 +15686,4,109000100,1 +15687,4,105000100,1 +15688,4,105000110,1 +15689,4,108000090,1 +15690,4,110000090,1 +15691,4,102000100,1 +15692,4,102000110,1 +15693,4,106000090,1 +15694,4,109000090,1 +15695,4,107000100,1 +15696,4,103000090,1 +15697,4,102000090,1 +15698,4,103000100,1 +15699,4,106000100,1 +15700,4,106000110,1 +15701,4,104000090,1 +15702,4,104000100,1 +15703,4,104000110,1 +15704,4,107000090,1 +15705,4,104000180,1 +15706,4,111000040,1 +15707,4,112000040,1 +15708,4,108000100,1 +15709,4,105000090,1 +15710,4,110000100,1 +15711,4,118000050,1 +15712,4,118000060,1 +15713,5,101000120,1 +15714,5,109000110,1 +15715,5,105000120,1 +15716,5,102000120,1 +15717,5,107000110,1 +15718,5,103000120,1 +15719,5,106000120,1 +15720,5,104000120,1 +15721,5,104000190,1 +15722,5,111000060,1 +15723,5,112000060,1 +15724,5,108000110,1 +15725,5,110000110,1 +15726,5,118000070,1 +15727,1,201000010,8 +15728,1,292000010,8 +15729,1,299000040,8 +15730,1,299000070,8 +15731,1,299000110,8 +15732,1,299000120,8 +15733,1,299000140,8 +15734,2,202000010,8 +15735,2,290000010,8 +15736,2,299000010,8 +15737,2,299000150,8 +15738,2,299000190,8 +15739,2,299000200,8 +15740,2,299000210,8 +15741,3,298000050,8 +15742,3,298000060,8 +15743,3,299000060,8 +15744,3,299000170,8 +15745,3,290000120,8 +15746,3,291000050,8 +15747,3,292000020,8 +15748,4,299000670,8 +15749,4,299000680,8 +15750,4,204000010,8 +15751,4,209000040,8 +15752,4,202000070,8 +15753,4,209000070,8 +15754,4,203000110,8 +15755,4,290000110,8 +15756,4,206000110,8 +15757,4,209000160,8 +15758,5,297000100,8 +15759,5,291000020,8 +15760,5,297000130,8 +15761,5,297000140,8 +15762,5,203000010,8 +15763,5,206000030,8 +15764,5,203000050,8 +15765,5,202000090,8 +15766,5,204000080,8 +15767,5,202000150,8 +15768,5,204000100,8 +15769,5,206000170,8 +15770,2,170003,3 +15771,2,180003,3 +15772,3,170004,3 +15773,3,180004,3 +15774,4,140000,3 +15775,5,150010,3 +15776,5,150020,3 +15777,5,150030,3 +15778,5,150040,3 +15780,3,101000050,1 +15781,3,101000060,1 +15782,3,101000080,1 +15783,3,101000040,1 +15784,3,109000060,1 +15785,3,109000070,1 +15786,3,109000080,1 +15787,3,105000060,1 +15788,3,105000070,1 +15789,3,105000080,1 +15790,3,104000050,1 +15791,3,106000050,1 +15792,3,102000060,1 +15793,3,102000070,1 +15794,3,102000080,1 +15795,3,103000050,1 +15796,3,105000050,1 +15797,3,107000060,1 +15798,3,107000070,1 +15799,3,107000080,1 +15800,3,108000050,1 +15801,3,109000050,1 +15802,3,103000060,1 +15803,3,103000070,1 +15804,3,103000080,1 +15805,3,110000050,1 +15806,3,106000060,1 +15807,3,106000070,1 +15808,3,106000080,1 +15809,3,101000070,1 +15810,3,110000060,1 +15811,3,104000060,1 +15812,3,104000070,1 +15813,3,104000080,1 +15814,3,102000050,1 +15815,3,104000170,1 +15816,3,104000260,1 +15817,3,111000010,1 +15818,3,111000020,1 +15819,3,111000030,1 +15820,3,112000020,1 +15821,3,112000030,1 +15822,3,108000060,1 +15823,3,108000070,1 +15824,3,108000080,1 +15825,3,107000050,1 +15826,3,112000010,1 +15827,3,110000070,1 +15828,3,110000080,1 +15829,3,118000020,1 +15830,3,118000030,1 +15831,3,118000040,1 +15832,4,101000090,1 +15833,4,101000100,1 +15834,4,101000110,1 +15835,4,109000100,1 +15836,4,105000100,1 +15837,4,105000110,1 +15838,4,108000090,1 +15839,4,110000090,1 +15840,4,102000100,1 +15841,4,102000110,1 +15842,4,106000090,1 +15843,4,109000090,1 +15844,4,107000100,1 +15845,4,103000090,1 +15846,4,102000090,1 +15847,4,103000100,1 +15848,4,106000100,1 +15849,4,106000110,1 +15850,4,104000090,1 +15851,4,104000100,1 +15852,4,104000110,1 +15853,4,107000090,1 +15854,4,104000180,1 +15855,4,111000040,1 +15856,4,112000040,1 +15857,4,108000100,1 +15858,4,105000090,1 +15859,4,110000100,1 +15860,4,118000050,1 +15861,4,118000060,1 +15862,5,101000120,1 +15863,5,109000110,1 +15864,5,105000120,1 +15865,5,102000120,1 +15866,5,107000110,1 +15867,5,103000120,1 +15868,5,106000120,1 +15869,5,104000120,1 +15870,5,104000190,1 +15871,5,111000060,1 +15872,5,112000060,1 +15873,5,108000110,1 +15874,5,110000110,1 +15875,5,118000070,1 +15876,1,201000010,8 +15877,1,292000010,8 +15878,1,299000040,8 +15879,1,299000070,8 +15880,1,299000110,8 +15881,1,299000120,8 +15882,1,299000140,8 +15883,2,202000010,8 +15884,2,290000010,8 +15885,2,299000010,8 +15886,2,299000150,8 +15887,2,299000190,8 +15888,2,299000200,8 +15889,2,299000210,8 +15890,3,298000050,8 +15891,3,298000060,8 +15892,3,299000060,8 +15893,3,299000170,8 +15894,3,290000120,8 +15895,3,291000050,8 +15896,3,292000020,8 +15897,4,299000670,8 +15898,4,299000680,8 +15899,4,204000010,8 +15900,4,209000040,8 +15901,4,202000070,8 +15902,4,209000070,8 +15903,4,203000110,8 +15904,4,290000110,8 +15905,4,206000110,8 +15906,4,209000160,8 +15907,5,297000100,8 +15908,5,291000020,8 +15909,5,297000130,8 +15910,5,297000140,8 +15911,5,203000010,8 +15912,5,206000030,8 +15913,5,203000050,8 +15914,5,202000090,8 +15915,5,204000080,8 +15916,5,202000150,8 +15917,5,204000100,8 +15918,5,206000170,8 +15919,3,170004,3 +15920,3,180004,3 +15921,4,140000,3 +15922,5,150010,3 +15923,5,150020,3 +15924,5,150030,3 +15925,5,150040,3 +15927,3,101000050,1 +15928,3,101000060,1 +15929,3,101000080,1 +15930,3,101000040,1 +15931,3,109000060,1 +15932,3,109000070,1 +15933,3,109000080,1 +15934,3,105000060,1 +15935,3,105000070,1 +15936,3,105000080,1 +15937,3,104000050,1 +15938,3,106000050,1 +15939,3,102000060,1 +15940,3,102000070,1 +15941,3,102000080,1 +15942,3,103000050,1 +15943,3,105000050,1 +15944,3,107000060,1 +15945,3,107000070,1 +15946,3,107000080,1 +15947,3,108000050,1 +15948,3,109000050,1 +15949,3,103000060,1 +15950,3,103000070,1 +15951,3,103000080,1 +15952,3,110000050,1 +15953,3,106000060,1 +15954,3,106000070,1 +15955,3,106000080,1 +15956,3,101000070,1 +15957,3,110000060,1 +15958,3,104000060,1 +15959,3,104000070,1 +15960,3,104000080,1 +15961,3,102000050,1 +15962,3,104000170,1 +15963,3,104000260,1 +15964,3,111000010,1 +15965,3,111000020,1 +15966,3,111000030,1 +15967,3,112000020,1 +15968,3,112000030,1 +15969,3,108000060,1 +15970,3,108000070,1 +15971,3,108000080,1 +15972,3,107000050,1 +15973,3,112000010,1 +15974,3,110000070,1 +15975,3,110000080,1 +15976,3,118000020,1 +15977,3,118000030,1 +15978,3,118000040,1 +15979,4,101000090,1 +15980,4,101000100,1 +15981,4,101000110,1 +15982,4,109000100,1 +15983,4,105000100,1 +15984,4,105000110,1 +15985,4,108000090,1 +15986,4,110000090,1 +15987,4,102000100,1 +15988,4,102000110,1 +15989,4,106000090,1 +15990,4,109000090,1 +15991,4,107000100,1 +15992,4,103000090,1 +15993,4,102000090,1 +15994,4,103000100,1 +15995,4,106000100,1 +15996,4,106000110,1 +15997,4,104000090,1 +15998,4,104000100,1 +15999,4,104000110,1 +16000,4,107000090,1 +16001,4,104000180,1 +16002,4,111000040,1 +16003,4,112000040,1 +16004,4,108000100,1 +16005,4,105000090,1 +16006,4,110000100,1 +16007,4,118000050,1 +16008,4,118000060,1 +16009,5,101000120,1 +16010,5,109000110,1 +16011,5,105000120,1 +16012,5,102000120,1 +16013,5,107000110,1 +16014,5,103000120,1 +16015,5,106000120,1 +16016,5,104000120,1 +16017,5,104000190,1 +16018,5,111000060,1 +16019,5,112000060,1 +16020,5,108000110,1 +16021,5,110000110,1 +16022,5,118000070,1 +16023,1,201000010,8 +16024,1,292000010,8 +16025,1,299000040,8 +16026,1,299000070,8 +16027,1,299000110,8 +16028,1,299000120,8 +16029,1,299000140,8 +16030,2,202000010,8 +16031,2,290000010,8 +16032,2,299000010,8 +16033,2,299000150,8 +16034,2,299000190,8 +16035,2,299000200,8 +16036,2,299000210,8 +16037,3,298000050,8 +16038,3,298000060,8 +16039,3,299000060,8 +16040,3,299000170,8 +16041,3,290000120,8 +16042,3,291000050,8 +16043,3,292000020,8 +16044,4,299000670,8 +16045,4,299000680,8 +16046,4,204000010,8 +16047,4,209000040,8 +16048,4,202000070,8 +16049,4,209000070,8 +16050,4,203000110,8 +16051,4,290000110,8 +16052,4,206000110,8 +16053,4,209000160,8 +16054,5,297000100,8 +16055,5,291000020,8 +16056,5,297000130,8 +16057,5,297000140,8 +16058,5,203000010,8 +16059,5,206000030,8 +16060,5,203000050,8 +16061,5,202000090,8 +16062,5,204000080,8 +16063,5,202000150,8 +16064,5,204000100,8 +16065,5,206000170,8 +16066,3,170004,3 +16067,3,180004,3 +16068,4,140000,3 +16069,5,150010,3 +16070,5,150020,3 +16071,5,150030,3 +16072,5,150040,3 +16074,3,101000050,1 +16075,3,101000060,1 +16076,3,101000080,1 +16077,3,101000040,1 +16078,3,109000060,1 +16079,3,109000070,1 +16080,3,109000080,1 +16081,3,105000060,1 +16082,3,105000070,1 +16083,3,105000080,1 +16084,3,104000050,1 +16085,3,106000050,1 +16086,3,102000060,1 +16087,3,102000070,1 +16088,3,102000080,1 +16089,3,103000050,1 +16090,3,105000050,1 +16091,3,107000060,1 +16092,3,107000070,1 +16093,3,107000080,1 +16094,3,108000050,1 +16095,3,109000050,1 +16096,3,103000060,1 +16097,3,103000070,1 +16098,3,103000080,1 +16099,3,110000050,1 +16100,3,106000060,1 +16101,3,106000070,1 +16102,3,106000080,1 +16103,3,101000070,1 +16104,3,110000060,1 +16105,3,104000060,1 +16106,3,104000070,1 +16107,3,104000080,1 +16108,3,102000050,1 +16109,3,104000170,1 +16110,3,104000260,1 +16111,3,111000010,1 +16112,3,111000020,1 +16113,3,111000030,1 +16114,3,112000020,1 +16115,3,112000030,1 +16116,3,108000060,1 +16117,3,108000070,1 +16118,3,108000080,1 +16119,3,107000050,1 +16120,3,112000010,1 +16121,3,110000070,1 +16122,3,110000080,1 +16123,3,118000020,1 +16124,3,118000030,1 +16125,3,118000040,1 +16126,4,101000090,1 +16127,4,101000100,1 +16128,4,101000110,1 +16129,4,109000100,1 +16130,4,105000100,1 +16131,4,105000110,1 +16132,4,108000090,1 +16133,4,110000090,1 +16134,4,102000100,1 +16135,4,102000110,1 +16136,4,106000090,1 +16137,4,109000090,1 +16138,4,107000100,1 +16139,4,103000090,1 +16140,4,102000090,1 +16141,4,103000100,1 +16142,4,106000100,1 +16143,4,106000110,1 +16144,4,104000090,1 +16145,4,104000100,1 +16146,4,104000110,1 +16147,4,107000090,1 +16148,4,104000180,1 +16149,4,111000040,1 +16150,4,112000040,1 +16151,4,108000100,1 +16152,4,105000090,1 +16153,4,110000100,1 +16154,4,118000050,1 +16155,4,118000060,1 +16156,5,101000120,1 +16157,5,109000110,1 +16158,5,105000120,1 +16159,5,102000120,1 +16160,5,107000110,1 +16161,5,103000120,1 +16162,5,106000120,1 +16163,5,104000120,1 +16164,5,104000190,1 +16165,5,111000060,1 +16166,5,112000060,1 +16167,5,108000110,1 +16168,5,110000110,1 +16169,5,118000070,1 +16170,1,201000010,8 +16171,1,292000010,8 +16172,1,299000040,8 +16173,1,299000070,8 +16174,1,299000110,8 +16175,1,299000120,8 +16176,1,299000140,8 +16177,2,202000010,8 +16178,2,290000010,8 +16179,2,299000010,8 +16180,2,299000150,8 +16181,2,299000190,8 +16182,2,299000200,8 +16183,2,299000210,8 +16184,3,298000050,8 +16185,3,298000060,8 +16186,3,299000060,8 +16187,3,299000170,8 +16188,3,290000120,8 +16189,3,291000050,8 +16190,3,292000020,8 +16191,4,299000670,8 +16192,4,299000680,8 +16193,4,204000010,8 +16194,4,209000040,8 +16195,4,202000070,8 +16196,4,209000070,8 +16197,4,203000110,8 +16198,4,290000110,8 +16199,4,206000110,8 +16200,4,209000160,8 +16201,5,297000100,8 +16202,5,291000020,8 +16203,5,297000130,8 +16204,5,297000140,8 +16205,5,203000010,8 +16206,5,206000030,8 +16207,5,203000050,8 +16208,5,202000090,8 +16209,5,204000080,8 +16210,5,202000150,8 +16211,5,204000100,8 +16212,5,206000170,8 +16213,3,170004,3 +16214,3,180004,3 +16215,4,140000,3 +16216,5,150010,3 +16217,5,150020,3 +16218,5,150030,3 +16219,5,150040,3 +16220,5,190000,3 +16221,5,200000,3 +16222,5,210000,3 +16224,1,101000010,1 +16225,1,102000010,1 +16226,1,103000010,1 +16227,1,104000010,1 +16228,1,105000010,1 +16229,1,106000010,1 +16230,1,107000010,1 +16231,1,108000010,1 +16232,1,109000010,1 +16233,1,110000010,1 +16234,2,101000020,1 +16235,2,101000030,1 +16236,2,102000020,1 +16237,2,102000030,1 +16238,2,102000040,1 +16239,2,103000020,1 +16240,2,103000030,1 +16241,2,103000040,1 +16242,2,104000020,1 +16243,2,104000030,1 +16244,2,104000040,1 +16245,2,105000020,1 +16246,2,105000030,1 +16247,2,105000040,1 +16248,2,106000020,1 +16249,2,106000030,1 +16250,2,106000040,1 +16251,2,107000020,1 +16252,2,107000030,1 +16253,2,107000040,1 +16254,2,108000020,1 +16255,2,108000030,1 +16256,2,108000040,1 +16257,2,109000020,1 +16258,2,109000030,1 +16259,2,109000040,1 +16260,2,110000020,1 +16261,2,110000030,1 +16262,2,110000040,1 +16263,2,118000010,1 +16264,3,101000050,1 +16265,3,101000060,1 +16266,3,101000080,1 +16267,3,101000040,1 +16268,3,109000060,1 +16269,3,109000070,1 +16270,3,109000080,1 +16271,3,105000060,1 +16272,3,105000070,1 +16273,3,105000080,1 +16274,3,104000050,1 +16275,3,106000050,1 +16276,3,102000060,1 +16277,3,102000070,1 +16278,3,102000080,1 +16279,3,103000050,1 +16280,3,105000050,1 +16281,3,107000060,1 +16282,3,107000070,1 +16283,3,107000080,1 +16284,3,108000050,1 +16285,3,109000050,1 +16286,3,103000060,1 +16287,3,103000070,1 +16288,3,103000080,1 +16289,3,110000050,1 +16290,3,106000060,1 +16291,3,106000070,1 +16292,3,106000080,1 +16293,3,101000070,1 +16294,3,110000060,1 +16295,3,104000060,1 +16296,3,104000070,1 +16297,3,104000080,1 +16298,3,102000050,1 +16299,3,104000170,1 +16300,3,104000260,1 +16301,3,111000010,1 +16302,3,111000020,1 +16303,3,111000030,1 +16304,3,112000020,1 +16305,3,112000030,1 +16306,3,108000060,1 +16307,3,108000070,1 +16308,3,108000080,1 +16309,3,107000050,1 +16310,3,112000010,1 +16311,3,110000070,1 +16312,3,110000080,1 +16313,3,118000020,1 +16314,3,118000030,1 +16315,3,118000040,1 +16316,4,101000090,1 +16317,4,101000100,1 +16318,4,101000110,1 +16319,4,109000100,1 +16320,4,105000100,1 +16321,4,105000110,1 +16322,4,108000090,1 +16323,4,110000090,1 +16324,4,102000100,1 +16325,4,102000110,1 +16326,4,106000090,1 +16327,4,109000090,1 +16328,4,107000100,1 +16329,4,103000090,1 +16330,4,102000090,1 +16331,4,103000100,1 +16332,4,106000100,1 +16333,4,106000110,1 +16334,4,104000090,1 +16335,4,104000100,1 +16336,4,104000110,1 +16337,4,107000090,1 +16338,4,104000180,1 +16339,4,111000040,1 +16340,4,112000040,1 +16341,4,108000100,1 +16342,4,105000090,1 +16343,4,110000100,1 +16344,4,118000050,1 +16345,4,118000060,1 +16346,5,101000120,1 +16347,5,109000110,1 +16348,5,105000120,1 +16349,5,102000120,1 +16350,5,107000110,1 +16351,5,103000120,1 +16352,5,106000120,1 +16353,5,104000120,1 +16354,5,104000190,1 +16355,5,111000060,1 +16356,5,112000060,1 +16357,5,108000110,1 +16358,5,110000110,1 +16359,5,118000070,1 +16360,1,201000010,8 +16361,1,292000010,8 +16362,1,299000040,8 +16363,1,299000070,8 +16364,1,299000110,8 +16365,1,299000120,8 +16366,1,299000140,8 +16367,2,202000010,8 +16368,2,290000010,8 +16369,2,299000010,8 +16370,2,299000150,8 +16371,2,299000190,8 +16372,2,299000200,8 +16373,2,299000210,8 +16374,3,298000050,8 +16375,3,298000060,8 +16376,3,299000060,8 +16377,3,299000170,8 +16378,3,290000120,8 +16379,3,291000050,8 +16380,3,292000020,8 +16381,4,299000670,8 +16382,4,299000680,8 +16383,4,204000010,8 +16384,4,209000040,8 +16385,4,202000070,8 +16386,4,209000070,8 +16387,4,203000110,8 +16388,4,290000110,8 +16389,4,206000110,8 +16390,4,209000160,8 +16391,4,209000190,8 +16392,5,297000100,8 +16393,5,291000020,8 +16394,5,297000130,8 +16395,5,297000140,8 +16396,5,203000010,8 +16397,5,206000030,8 +16398,5,203000050,8 +16399,5,202000090,8 +16400,5,204000080,8 +16401,5,202000150,8 +16402,5,204000100,8 +16403,5,206000170,8 +16404,5,204000200,8 +16405,1,170002,3 +16406,1,180002,3 +16407,2,170003,3 +16408,2,180003,3 +16409,3,170004,3 +16410,3,180004,3 +16411,4,140000,3 +16412,5,150010,3 +16413,5,150020,3 +16414,5,150030,3 +16415,5,150040,3 +16417,1,101000010,1 +16418,1,102000010,1 +16419,1,103000010,1 +16420,1,104000010,1 +16421,1,105000010,1 +16422,1,106000010,1 +16423,1,107000010,1 +16424,1,108000010,1 +16425,1,109000010,1 +16426,1,110000010,1 +16427,2,101000020,1 +16428,2,101000030,1 +16429,2,102000020,1 +16430,2,102000030,1 +16431,2,102000040,1 +16432,2,103000020,1 +16433,2,103000030,1 +16434,2,103000040,1 +16435,2,104000020,1 +16436,2,104000030,1 +16437,2,104000040,1 +16438,2,105000020,1 +16439,2,105000030,1 +16440,2,105000040,1 +16441,2,106000020,1 +16442,2,106000030,1 +16443,2,106000040,1 +16444,2,107000020,1 +16445,2,107000030,1 +16446,2,107000040,1 +16447,2,108000020,1 +16448,2,108000030,1 +16449,2,108000040,1 +16450,2,109000020,1 +16451,2,109000030,1 +16452,2,109000040,1 +16453,2,110000020,1 +16454,2,110000030,1 +16455,2,110000040,1 +16456,2,118000010,1 +16457,3,101000050,1 +16458,3,101000060,1 +16459,3,101000080,1 +16460,3,101000040,1 +16461,3,109000060,1 +16462,3,109000070,1 +16463,3,109000080,1 +16464,3,105000060,1 +16465,3,105000070,1 +16466,3,105000080,1 +16467,3,104000050,1 +16468,3,106000050,1 +16469,3,102000060,1 +16470,3,102000070,1 +16471,3,102000080,1 +16472,3,103000050,1 +16473,3,105000050,1 +16474,3,107000060,1 +16475,3,107000070,1 +16476,3,107000080,1 +16477,3,108000050,1 +16478,3,109000050,1 +16479,3,103000060,1 +16480,3,103000070,1 +16481,3,103000080,1 +16482,3,110000050,1 +16483,3,106000060,1 +16484,3,106000070,1 +16485,3,106000080,1 +16486,3,101000070,1 +16487,3,110000060,1 +16488,3,104000060,1 +16489,3,104000070,1 +16490,3,104000080,1 +16491,3,102000050,1 +16492,3,104000170,1 +16493,3,104000260,1 +16494,3,111000010,1 +16495,3,111000020,1 +16496,3,111000030,1 +16497,3,112000020,1 +16498,3,112000030,1 +16499,3,108000060,1 +16500,3,108000070,1 +16501,3,108000080,1 +16502,3,107000050,1 +16503,3,112000010,1 +16504,3,110000070,1 +16505,3,110000080,1 +16506,3,118000020,1 +16507,3,118000030,1 +16508,3,118000040,1 +16509,4,101000090,1 +16510,4,101000100,1 +16511,4,101000110,1 +16512,4,109000100,1 +16513,4,105000100,1 +16514,4,105000110,1 +16515,4,108000090,1 +16516,4,110000090,1 +16517,4,102000100,1 +16518,4,102000110,1 +16519,4,106000090,1 +16520,4,109000090,1 +16521,4,107000100,1 +16522,4,103000090,1 +16523,4,102000090,1 +16524,4,103000100,1 +16525,4,106000100,1 +16526,4,106000110,1 +16527,4,104000090,1 +16528,4,104000100,1 +16529,4,104000110,1 +16530,4,107000090,1 +16531,4,104000180,1 +16532,4,111000040,1 +16533,4,112000040,1 +16534,4,108000100,1 +16535,4,105000090,1 +16536,4,110000100,1 +16537,4,118000050,1 +16538,4,118000060,1 +16539,5,101000120,1 +16540,5,109000110,1 +16541,5,105000120,1 +16542,5,102000120,1 +16543,5,107000110,1 +16544,5,103000120,1 +16545,5,106000120,1 +16546,5,104000120,1 +16547,5,104000190,1 +16548,5,111000060,1 +16549,5,112000060,1 +16550,5,108000110,1 +16551,5,110000110,1 +16552,5,118000070,1 +16553,1,201000010,8 +16554,1,292000010,8 +16555,1,299000040,8 +16556,1,299000070,8 +16557,1,299000110,8 +16558,1,299000120,8 +16559,1,299000140,8 +16560,2,202000010,8 +16561,2,290000010,8 +16562,2,299000010,8 +16563,2,299000150,8 +16564,2,299000190,8 +16565,2,299000200,8 +16566,2,299000210,8 +16567,3,298000050,8 +16568,3,298000060,8 +16569,3,299000060,8 +16570,3,299000170,8 +16571,3,290000120,8 +16572,3,291000050,8 +16573,3,292000020,8 +16574,4,299000670,8 +16575,4,299000680,8 +16576,4,204000010,8 +16577,4,209000040,8 +16578,4,202000070,8 +16579,4,209000070,8 +16580,4,203000110,8 +16581,4,290000110,8 +16582,4,206000110,8 +16583,4,209000160,8 +16584,4,209000190,8 +16585,5,297000100,8 +16586,5,291000020,8 +16587,5,297000130,8 +16588,5,297000140,8 +16589,5,203000010,8 +16590,5,206000030,8 +16591,5,203000050,8 +16592,5,202000090,8 +16593,5,204000080,8 +16594,5,202000150,8 +16595,5,204000100,8 +16596,5,206000170,8 +16597,5,204000200,8 +16598,2,170003,3 +16599,2,180003,3 +16600,3,170004,3 +16601,3,180004,3 +16602,4,140000,3 +16603,5,150010,3 +16604,5,150020,3 +16605,5,150030,3 +16606,5,150040,3 +16608,3,101000050,1 +16609,3,101000060,1 +16610,3,101000080,1 +16611,3,101000040,1 +16612,3,109000060,1 +16613,3,109000070,1 +16614,3,109000080,1 +16615,3,105000060,1 +16616,3,105000070,1 +16617,3,105000080,1 +16618,3,104000050,1 +16619,3,106000050,1 +16620,3,102000060,1 +16621,3,102000070,1 +16622,3,102000080,1 +16623,3,103000050,1 +16624,3,105000050,1 +16625,3,107000060,1 +16626,3,107000070,1 +16627,3,107000080,1 +16628,3,108000050,1 +16629,3,109000050,1 +16630,3,103000060,1 +16631,3,103000070,1 +16632,3,103000080,1 +16633,3,110000050,1 +16634,3,106000060,1 +16635,3,106000070,1 +16636,3,106000080,1 +16637,3,101000070,1 +16638,3,110000060,1 +16639,3,104000060,1 +16640,3,104000070,1 +16641,3,104000080,1 +16642,3,102000050,1 +16643,3,104000170,1 +16644,3,104000260,1 +16645,3,111000010,1 +16646,3,111000020,1 +16647,3,111000030,1 +16648,3,112000020,1 +16649,3,112000030,1 +16650,3,108000060,1 +16651,3,108000070,1 +16652,3,108000080,1 +16653,3,107000050,1 +16654,3,112000010,1 +16655,3,110000070,1 +16656,3,110000080,1 +16657,3,118000020,1 +16658,3,118000030,1 +16659,3,118000040,1 +16660,4,101000090,1 +16661,4,101000100,1 +16662,4,101000110,1 +16663,4,109000100,1 +16664,4,105000100,1 +16665,4,105000110,1 +16666,4,108000090,1 +16667,4,110000090,1 +16668,4,102000100,1 +16669,4,102000110,1 +16670,4,106000090,1 +16671,4,109000090,1 +16672,4,107000100,1 +16673,4,103000090,1 +16674,4,102000090,1 +16675,4,103000100,1 +16676,4,106000100,1 +16677,4,106000110,1 +16678,4,104000090,1 +16679,4,104000100,1 +16680,4,104000110,1 +16681,4,107000090,1 +16682,4,104000180,1 +16683,4,111000040,1 +16684,4,112000040,1 +16685,4,108000100,1 +16686,4,105000090,1 +16687,4,110000100,1 +16688,4,118000050,1 +16689,4,118000060,1 +16690,5,101000120,1 +16691,5,109000110,1 +16692,5,105000120,1 +16693,5,102000120,1 +16694,5,107000110,1 +16695,5,103000120,1 +16696,5,106000120,1 +16697,5,104000120,1 +16698,5,104000190,1 +16699,5,111000060,1 +16700,5,112000060,1 +16701,5,108000110,1 +16702,5,110000110,1 +16703,5,118000070,1 +16704,1,201000010,8 +16705,1,292000010,8 +16706,1,299000040,8 +16707,1,299000070,8 +16708,1,299000110,8 +16709,1,299000120,8 +16710,1,299000140,8 +16711,2,202000010,8 +16712,2,290000010,8 +16713,2,299000010,8 +16714,2,299000150,8 +16715,2,299000190,8 +16716,2,299000200,8 +16717,2,299000210,8 +16718,3,298000050,8 +16719,3,298000060,8 +16720,3,299000060,8 +16721,3,299000170,8 +16722,3,290000120,8 +16723,3,291000050,8 +16724,3,292000020,8 +16725,4,299000670,8 +16726,4,299000680,8 +16727,4,204000010,8 +16728,4,209000040,8 +16729,4,202000070,8 +16730,4,209000070,8 +16731,4,203000110,8 +16732,4,290000110,8 +16733,4,206000110,8 +16734,4,209000160,8 +16735,4,209000190,8 +16736,5,297000100,8 +16737,5,291000020,8 +16738,5,297000130,8 +16739,5,297000140,8 +16740,5,203000010,8 +16741,5,206000030,8 +16742,5,203000050,8 +16743,5,202000090,8 +16744,5,204000080,8 +16745,5,202000150,8 +16746,5,204000100,8 +16747,5,206000170,8 +16748,5,204000200,8 +16749,3,170004,3 +16750,3,180004,3 +16751,4,140000,3 +16752,5,150010,3 +16753,5,150020,3 +16754,5,150030,3 +16755,5,150040,3 +16757,3,101000050,1 +16758,3,101000060,1 +16759,3,101000080,1 +16760,3,101000040,1 +16761,3,109000060,1 +16762,3,109000070,1 +16763,3,109000080,1 +16764,3,105000060,1 +16765,3,105000070,1 +16766,3,105000080,1 +16767,3,104000050,1 +16768,3,106000050,1 +16769,3,102000060,1 +16770,3,102000070,1 +16771,3,102000080,1 +16772,3,103000050,1 +16773,3,105000050,1 +16774,3,107000060,1 +16775,3,107000070,1 +16776,3,107000080,1 +16777,3,108000050,1 +16778,3,109000050,1 +16779,3,103000060,1 +16780,3,103000070,1 +16781,3,103000080,1 +16782,3,110000050,1 +16783,3,106000060,1 +16784,3,106000070,1 +16785,3,106000080,1 +16786,3,101000070,1 +16787,3,110000060,1 +16788,3,104000060,1 +16789,3,104000070,1 +16790,3,104000080,1 +16791,3,102000050,1 +16792,3,104000170,1 +16793,3,104000260,1 +16794,3,111000010,1 +16795,3,111000020,1 +16796,3,111000030,1 +16797,3,112000020,1 +16798,3,112000030,1 +16799,3,108000060,1 +16800,3,108000070,1 +16801,3,108000080,1 +16802,3,107000050,1 +16803,3,112000010,1 +16804,3,110000070,1 +16805,3,110000080,1 +16806,3,118000020,1 +16807,3,118000030,1 +16808,3,118000040,1 +16809,4,101000090,1 +16810,4,101000100,1 +16811,4,101000110,1 +16812,4,109000100,1 +16813,4,105000100,1 +16814,4,105000110,1 +16815,4,108000090,1 +16816,4,110000090,1 +16817,4,102000100,1 +16818,4,102000110,1 +16819,4,106000090,1 +16820,4,109000090,1 +16821,4,107000100,1 +16822,4,103000090,1 +16823,4,102000090,1 +16824,4,103000100,1 +16825,4,106000100,1 +16826,4,106000110,1 +16827,4,104000090,1 +16828,4,104000100,1 +16829,4,104000110,1 +16830,4,107000090,1 +16831,4,104000180,1 +16832,4,111000040,1 +16833,4,112000040,1 +16834,4,108000100,1 +16835,4,105000090,1 +16836,4,110000100,1 +16837,4,118000050,1 +16838,4,118000060,1 +16839,5,101000120,1 +16840,5,109000110,1 +16841,5,105000120,1 +16842,5,102000120,1 +16843,5,107000110,1 +16844,5,103000120,1 +16845,5,106000120,1 +16846,5,104000120,1 +16847,5,104000190,1 +16848,5,111000060,1 +16849,5,112000060,1 +16850,5,108000110,1 +16851,5,110000110,1 +16852,5,118000070,1 +16853,1,201000010,8 +16854,1,292000010,8 +16855,1,299000040,8 +16856,1,299000070,8 +16857,1,299000110,8 +16858,1,299000120,8 +16859,1,299000140,8 +16860,2,202000010,8 +16861,2,290000010,8 +16862,2,299000010,8 +16863,2,299000150,8 +16864,2,299000190,8 +16865,2,299000200,8 +16866,2,299000210,8 +16867,3,298000050,8 +16868,3,298000060,8 +16869,3,299000060,8 +16870,3,299000170,8 +16871,3,290000120,8 +16872,3,291000050,8 +16873,3,292000020,8 +16874,4,299000670,8 +16875,4,299000680,8 +16876,4,204000010,8 +16877,4,209000040,8 +16878,4,202000070,8 +16879,4,209000070,8 +16880,4,203000110,8 +16881,4,290000110,8 +16882,4,206000110,8 +16883,4,209000160,8 +16884,4,209000190,8 +16885,5,297000100,8 +16886,5,291000020,8 +16887,5,297000130,8 +16888,5,297000140,8 +16889,5,203000010,8 +16890,5,206000030,8 +16891,5,203000050,8 +16892,5,202000090,8 +16893,5,204000080,8 +16894,5,202000150,8 +16895,5,204000100,8 +16896,5,206000170,8 +16897,5,204000200,8 +16898,3,170004,3 +16899,3,180004,3 +16900,4,140000,3 +16901,5,150010,3 +16902,5,150020,3 +16903,5,150030,3 +16904,5,150040,3 +16906,3,101000050,1 +16907,3,101000060,1 +16908,3,101000080,1 +16909,3,101000040,1 +16910,3,109000060,1 +16911,3,109000070,1 +16912,3,109000080,1 +16913,3,105000060,1 +16914,3,105000070,1 +16915,3,105000080,1 +16916,3,104000050,1 +16917,3,106000050,1 +16918,3,102000060,1 +16919,3,102000070,1 +16920,3,102000080,1 +16921,3,103000050,1 +16922,3,105000050,1 +16923,3,107000060,1 +16924,3,107000070,1 +16925,3,107000080,1 +16926,3,108000050,1 +16927,3,109000050,1 +16928,3,103000060,1 +16929,3,103000070,1 +16930,3,103000080,1 +16931,3,110000050,1 +16932,3,106000060,1 +16933,3,106000070,1 +16934,3,106000080,1 +16935,3,101000070,1 +16936,3,110000060,1 +16937,3,104000060,1 +16938,3,104000070,1 +16939,3,104000080,1 +16940,3,102000050,1 +16941,3,104000170,1 +16942,3,104000260,1 +16943,3,111000010,1 +16944,3,111000020,1 +16945,3,111000030,1 +16946,3,112000020,1 +16947,3,112000030,1 +16948,3,108000060,1 +16949,3,108000070,1 +16950,3,108000080,1 +16951,3,107000050,1 +16952,3,112000010,1 +16953,3,110000070,1 +16954,3,110000080,1 +16955,3,118000020,1 +16956,3,118000030,1 +16957,3,118000040,1 +16958,4,101000090,1 +16959,4,101000100,1 +16960,4,101000110,1 +16961,4,109000100,1 +16962,4,105000100,1 +16963,4,105000110,1 +16964,4,108000090,1 +16965,4,110000090,1 +16966,4,102000100,1 +16967,4,102000110,1 +16968,4,106000090,1 +16969,4,109000090,1 +16970,4,107000100,1 +16971,4,103000090,1 +16972,4,102000090,1 +16973,4,103000100,1 +16974,4,106000100,1 +16975,4,106000110,1 +16976,4,104000090,1 +16977,4,104000100,1 +16978,4,104000110,1 +16979,4,107000090,1 +16980,4,104000180,1 +16981,4,111000040,1 +16982,4,112000040,1 +16983,4,108000100,1 +16984,4,105000090,1 +16985,4,110000100,1 +16986,4,118000050,1 +16987,4,118000060,1 +16988,5,101000120,1 +16989,5,109000110,1 +16990,5,105000120,1 +16991,5,102000120,1 +16992,5,107000110,1 +16993,5,103000120,1 +16994,5,106000120,1 +16995,5,104000120,1 +16996,5,104000190,1 +16997,5,111000060,1 +16998,5,112000060,1 +16999,5,108000110,1 +17000,5,110000110,1 +17001,5,118000070,1 +17002,1,201000010,8 +17003,1,292000010,8 +17004,1,299000040,8 +17005,1,299000070,8 +17006,1,299000110,8 +17007,1,299000120,8 +17008,1,299000140,8 +17009,2,202000010,8 +17010,2,290000010,8 +17011,2,299000010,8 +17012,2,299000150,8 +17013,2,299000190,8 +17014,2,299000200,8 +17015,2,299000210,8 +17016,3,298000050,8 +17017,3,298000060,8 +17018,3,299000060,8 +17019,3,299000170,8 +17020,3,290000120,8 +17021,3,291000050,8 +17022,3,292000020,8 +17023,4,299000670,8 +17024,4,299000680,8 +17025,4,204000010,8 +17026,4,209000040,8 +17027,4,202000070,8 +17028,4,209000070,8 +17029,4,203000110,8 +17030,4,290000110,8 +17031,4,206000110,8 +17032,4,209000160,8 +17033,4,209000190,8 +17034,5,297000100,8 +17035,5,291000020,8 +17036,5,297000130,8 +17037,5,297000140,8 +17038,5,203000010,8 +17039,5,206000030,8 +17040,5,203000050,8 +17041,5,202000090,8 +17042,5,204000080,8 +17043,5,202000150,8 +17044,5,204000100,8 +17045,5,206000170,8 +17046,5,204000200,8 +17047,3,170004,3 +17048,3,180004,3 +17049,4,140000,3 +17050,5,150010,3 +17051,5,150020,3 +17052,5,150030,3 +17053,5,150040,3 +17054,5,190000,3 +17055,5,200000,3 +17057,3,118000020,1 +17058,3,118000030,1 +17059,3,118000040,1 +17060,3,106000060,1 +17061,3,106000070,1 +17062,3,106000080,1 +17063,3,101000070,1 +17064,3,110000060,1 +17065,3,104000060,1 +17066,3,104000070,1 +17067,3,104000080,1 +17068,3,102000050,1 +17069,3,104000170,1 +17070,3,104000260,1 +17071,4,118000050,1 +17072,4,118000060,1 +17073,4,106000100,1 +17074,4,106000110,1 +17075,4,104000090,1 +17076,4,104000100,1 +17077,4,104000110,1 +17078,4,104000270,1 +17079,4,107000090,1 +17080,4,104000180,1 +17081,5,118000070,1 +17082,5,106000120,1 +17083,5,104000120,1 +17084,5,104000190,1 +17085,1,103000000,2 +17086,2,103000001,2 +17087,3,103000002,2 +17088,4,103000003,2 +17089,5,103000004,2 +17090,1,111000000,2 +17091,2,111000001,2 +17092,3,111000002,2 +17093,4,111000003,2 +17094,5,111000004,2 +17095,1,115000000,2 +17096,2,115000001,2 +17097,3,115000002,2 +17098,4,115000003,2 +17099,5,115000004,2 +17100,3,170004,3 +17101,4,170005,3 +17102,3,180004,3 +17103,4,180005,3 +17104,4,140000,3 +17105,4,150010,3 +17106,4,150020,3 +17107,4,150030,3 +17108,4,150040,3 +17110,3,111000010,1 +17111,3,111000020,1 +17112,3,111000030,1 +17113,3,112000020,1 +17114,3,112000030,1 +17115,3,108000060,1 +17116,3,108000070,1 +17117,3,108000080,1 +17118,3,107000050,1 +17119,3,112000010,1 +17120,3,110000070,1 +17121,3,110000080,1 +17122,4,111000040,1 +17123,4,112000040,1 +17124,4,108000100,1 +17125,4,105000090,1 +17126,4,110000100,1 +17127,5,111000060,1 +17128,5,112000060,1 +17129,5,108000110,1 +17130,5,110000110,1 +17131,1,108000000,2 +17132,2,108000001,2 +17133,3,108000002,2 +17134,4,108000003,2 +17135,5,108000004,2 +17136,1,107000000,2 +17137,2,107000001,2 +17138,3,107000002,2 +17139,4,107000003,2 +17140,5,107000004,2 +17141,1,120000000,2 +17142,2,120000001,2 +17143,3,120000002,2 +17144,4,120000003,2 +17145,5,120000004,2 +17146,4,110024,3 +17147,4,110034,3 +17148,4,110044,3 +17149,4,110054,3 +17150,3,110060,3 +17151,3,110070,3 +17152,3,110080,3 +17153,3,110090,3 +17154,3,110100,3 +17155,3,110110,3 +17156,3,110120,3 +17157,3,110130,3 +17158,3,110140,3 +17159,3,110150,3 +17160,3,110160,3 +17161,3,110170,3 +17162,3,110180,3 +17163,3,110190,3 +17164,3,110200,3 +17165,3,110210,3 +17166,3,110220,3 +17167,3,110230,3 +17168,3,110240,3 +17169,3,110250,3 +17170,3,110260,3 +17171,3,110270,3 +17172,3,110620,3 +17173,3,110670,3 +17174,4,140000,3 +17175,4,150010,3 +17176,4,150020,3 +17177,4,150030,3 +17178,4,150040,3 +17180,3,101000050,1 +17181,3,101000060,1 +17182,3,101000080,1 +17183,3,101000040,1 +17184,3,109000060,1 +17185,3,109000070,1 +17186,3,109000080,1 +17187,3,105000060,1 +17188,3,105000070,1 +17189,3,105000080,1 +17190,3,104000050,1 +17191,3,106000050,1 +17192,4,101000090,1 +17193,4,101000100,1 +17194,4,101000110,1 +17195,4,109000100,1 +17196,4,105000100,1 +17197,4,105000110,1 +17198,4,108000090,1 +17199,4,110000090,1 +17200,5,101000120,1 +17201,5,109000110,1 +17202,5,105000120,1 +17203,1,101000000,2 +17204,2,101000001,2 +17205,3,101000002,2 +17206,4,101000008,2 +17207,5,101000004,2 +17208,1,109000000,2 +17209,2,109000001,2 +17210,3,109000002,2 +17211,4,109000003,2 +17212,5,109000004,2 +17213,4,120024,3 +17214,4,120034,3 +17215,4,120044,3 +17216,4,120054,3 +17217,3,120241,3 +17218,3,120251,3 +17219,3,120261,3 +17220,3,120271,3 +17221,3,120300,3 +17222,3,120310,3 +17223,3,120320,3 +17224,3,120330,3 +17225,3,120340,3 +17226,3,120350,3 +17227,3,120360,3 +17228,3,120370,3 +17229,3,120380,3 +17230,3,120390,3 +17231,3,120400,3 +17232,3,120410,3 +17233,3,120420,3 +17234,3,120430,3 +17235,3,120450,3 +17236,3,120460,3 +17237,3,120550,3 +17238,3,120560,3 +17239,3,120570,3 +17240,3,120990,3 +17241,3,121000,3 +17242,3,121010,3 +17243,3,121020,3 +17244,3,121100,3 +17245,4,140000,3 +17246,4,150010,3 +17247,4,150020,3 +17248,4,150030,3 +17249,4,150040,3 +17251,3,102000060,1 +17252,3,102000070,1 +17253,3,102000080,1 +17254,3,103000050,1 +17255,3,105000050,1 +17256,3,107000060,1 +17257,3,107000070,1 +17258,3,107000080,1 +17259,3,108000050,1 +17260,3,109000050,1 +17261,3,103000060,1 +17262,3,103000070,1 +17263,3,103000080,1 +17264,3,110000050,1 +17265,4,102000100,1 +17266,4,102000110,1 +17267,4,102000350,1 +17268,4,106000090,1 +17269,4,109000090,1 +17270,4,107000100,1 +17271,4,103000090,1 +17272,4,102000090,1 +17273,4,103000100,1 +17274,5,102000120,1 +17275,5,107000110,1 +17276,5,103000120,1 +17277,1,102000000,2 +17278,2,102000001,2 +17279,3,102000002,2 +17280,4,102000003,2 +17281,5,102000004,2 +17282,1,105000000,2 +17283,2,105000001,2 +17284,3,105000002,2 +17285,4,105000003,2 +17286,5,105000004,2 +17287,1,112000000,2 +17288,2,112000001,2 +17289,3,112000002,2 +17290,4,112000003,2 +17291,5,112000004,2 +17292,4,130024,3 +17293,4,130034,3 +17294,4,130044,3 +17295,4,130054,3 +17296,3,130060,3 +17297,3,130070,3 +17298,3,130080,3 +17299,3,130090,3 +17300,3,130100,3 +17301,3,130110,3 +17302,3,130120,3 +17303,3,130130,3 +17304,3,130140,3 +17305,3,130150,3 +17306,3,130160,3 +17307,3,130170,3 +17308,3,130180,3 +17309,3,130190,3 +17310,3,130200,3 +17311,3,130420,3 +17312,3,130510,3 +17313,3,130520,3 +17314,3,130531,3 +17315,3,130540,3 +17316,3,130660,3 +17317,3,130700,3 +17318,3,130790,3 +17319,3,130800,3 +17320,3,131130,3 +17321,4,140000,3 +17322,4,150010,3 +17323,4,150020,3 +17324,4,150030,3 +17325,4,150040,3 +17327,4,101000250,1 +17328,4,102000260,1 +17329,4,103000220,1 +17330,4,104000250,1 +17331,4,105000210,1 +17332,4,106000210,1 +17333,4,107000140,1 +17334,4,108000150,1 +17335,4,109000230,1 +17336,4,110000170,1 +17337,4,111000140,1 +17338,4,112000110,1 +17339,4,118000140,1 +17340,5,101000300,1 +17341,5,102000360,1 +17342,5,103000310,1 +17343,5,104000370,1 +17344,5,109000290,1 +17345,5,101000310,1 +17346,5,102000410,1 +17347,1,201000010,8 +17348,1,292000010,8 +17349,1,299000040,8 +17350,1,299000070,8 +17351,1,299000110,8 +17352,1,299000120,8 +17353,1,299000140,8 +17354,2,202000010,8 +17355,2,290000010,8 +17356,2,299000010,8 +17357,2,299000150,8 +17358,2,299000190,8 +17359,2,299000200,8 +17360,2,299000210,8 +17361,3,290000120,8 +17362,3,291000050,8 +17363,3,292000020,8 +17364,3,298000050,8 +17365,3,298000060,8 +17366,3,299000060,8 +17367,3,299000170,8 +17368,4,201000170,8 +17369,4,202000070,8 +17370,4,202000370,8 +17371,4,202000400,8 +17372,4,202000440,8 +17373,4,203000110,8 +17374,4,203000270,8 +17375,4,203000350,8 +17376,4,204000010,8 +17377,4,204000370,8 +17378,4,205000220,8 +17379,4,206000110,8 +17380,4,206000240,8 +17381,4,206000270,8 +17382,4,209000040,8 +17383,4,209000070,8 +17384,4,209000160,8 +17385,4,209000190,8 +17386,4,209000240,8 +17387,4,215000150,8 +17388,4,290000110,8 +17389,4,290000130,8 +17390,4,298000120,8 +17391,4,299000670,8 +17392,4,299000680,8 +17393,4,201000090,8 +17394,4,202000160,8 +17395,4,203000150,8 +17396,4,204000110,8 +17397,4,205000110,8 +17398,4,206000120,8 +17399,4,211000060,8 +17400,4,212000050,8 +17401,4,299000440,8 +17402,4,299000450,8 +17403,4,299000460,8 +17404,4,299000470,8 +17405,4,299000480,8 +17406,4,299000550,8 +17407,4,299000560,8 +17408,4,299000570,8 +17409,4,299000580,8 +17410,4,299000590,8 +17411,4,299000600,8 +17412,4,299000610,8 +17413,4,299000620,8 +17414,4,299000630,8 +17415,4,299000640,8 +17416,4,299000650,8 +17417,4,299000230,8 +17418,4,299000240,8 +17419,4,299000250,8 +17420,4,299000260,8 +17421,4,299000270,8 +17422,4,299000280,8 +17423,4,299000290,8 +17424,4,299000300,8 +17425,4,299000310,8 +17426,4,299000320,8 +17427,4,299000330,8 +17428,4,299000340,8 +17429,4,299000350,8 +17430,4,299000360,8 +17431,4,299000370,8 +17432,4,299000380,8 +17433,4,299000390,8 +17434,4,299000400,8 +17435,4,215000050,8 +17436,4,216000060,8 +17437,4,217000020,8 +17438,4,218000030,8 +17439,4,299000500,8 +17440,4,299000510,8 +17441,4,299000520,8 +17442,4,299000530,8 +17443,4,299000540,8 +17444,4,299000660,8 +17445,5,202000090,8 +17446,5,202000150,8 +17447,5,203000010,8 +17448,5,203000050,8 +17449,5,203000260,8 +17450,5,204000080,8 +17451,5,204000100,8 +17452,5,204000200,8 +17453,5,204000270,8 +17454,5,206000030,8 +17455,5,206000170,8 +17456,5,209000320,8 +17457,5,220000080,8 +17458,5,290000160,8 +17459,5,291000020,8 +17460,5,297000100,8 +17461,5,297000130,8 +17462,5,297000140,8 +17463,5,298000110,8 +17464,5,202000290,8 +17465,5,203000240,8 +17466,5,204000210,8 +17467,5,205000150,8 +17468,5,206000200,8 +17469,5,211000090,8 +17470,5,212000060,8 +17471,5,299000800,8 +17472,5,299000810,8 +17473,5,299000820,8 +17474,5,299000840,8 +17475,5,299000850,8 +17476,5,299000860,8 +17477,5,299000870,8 +17478,5,299000880,8 +17479,5,299000890,8 +17480,5,201000120,8 +17481,5,202000240,8 +17482,5,211000080,8 +17483,5,299000730,8 +17484,5,299000740,8 +17485,5,299000750,8 +17486,5,299000760,8 +17487,5,299000770,8 +17488,5,201000130,8 +17489,5,299000830,8 +17490,5,202000280,8 +17491,5,204000220,8 +17492,5,201000150,8 +17493,5,202000350,8 +17494,5,205000200,8 +17495,5,299001050,8 +17496,5,299001060,8 +17497,5,201000200,8 +17498,5,202000430,8 +17499,5,211000150,8 +17500,5,299001170,8 +17501,5,299001180,8 +17502,5,202000270,8 +17503,5,206000190,8 +17504,5,220000060,8 +17505,5,201000140,8 +17506,5,203000230,8 +17507,5,205000160,8 +17508,5,204000230,8 +17509,5,209000200,8 +17510,5,299000900,8 +17511,5,202000340,8 +17512,5,203000280,8 +17513,5,205000190,8 +17514,5,208000040,8 +17515,5,211000110,8 +17516,5,220000070,8 +17517,5,202000420,8 +17518,5,203000340,8 +17519,5,204000360,8 +17520,5,211000140,8 +17521,5,212000090,8 +17522,5,299000920,8 +17523,5,299000930,8 +17524,5,299000940,8 +17525,5,299000950,8 +17526,5,299000960,8 +17527,5,298000130,8 +17528,5,202000380,8 +17529,5,203000300,8 +17530,5,204000320,8 +17531,5,205000230,8 +17532,5,206000250,8 +17533,5,209000280,8 +17534,5,210000030,8 +17535,5,211000120,8 +17536,5,212000070,8 +17537,5,215000130,8 +17538,5,216000130,8 +17539,5,217000080,8 +17540,5,218000090,8 +17541,5,299001070,8 +17542,5,299001080,8 +17543,5,299001090,8 +17544,5,215000120,8 +17545,5,202000390,8 +17546,5,203000310,8 +17547,5,204000330,8 +17548,5,205000240,8 +17549,5,206000260,8 +17550,5,209000290,8 +17551,5,210000040,8 +17552,5,211000130,8 +17553,5,212000080,8 +17554,5,215000140,8 +17555,5,216000140,8 +17556,5,217000090,8 +17557,5,218000100,8 +17558,5,220000090,8 +17559,5,299001100,8 +17560,5,299001110,8 +17561,5,299001120,8 +17562,5,299001130,8 +17563,5,299001140,8 +17564,5,299001150,8 +17565,5,299000430,8 +17566,5,299000690,8 +17567,5,299000710,8 +17568,5,299000720,8 +17569,5,215000100,8 +17570,5,216000090,8 +17571,5,217000070,8 +17572,5,218000080,8 +17573,5,299000970,8 +17574,5,299000980,8 +17575,5,299000990,8 +17576,5,299001000,8 +17577,5,299001010,8 +17578,5,299001020,8 +17579,5,299001030,8 +17580,5,299001040,8 +17581,3,101000006,2 +17582,3,103000005,2 +17583,4,101000003,2 +17584,4,102000005,2 +17585,4,107000005,2 +17586,4,112000006,2 +17587,5,101000009,2 +17588,5,101000011,2 +17589,5,101000012,2 +17590,5,101000013,2 +17591,5,101000014,2 +17592,5,101000015,2 +17593,5,101000016,2 +17594,5,101000017,2 +17595,5,101000018,2 +17596,5,102000008,2 +17597,5,102000009,2 +17598,5,107000007,2 +17599,5,109000008,2 +17600,5,111000007,2 +17601,5,111000008,2 +17602,5,112000008,2 +17603,5,120000008,2 +17605,2,118000010,1 +17606,3,118000020,1 +17607,3,118000030,1 +17608,3,118000040,1 +17609,4,118000050,1 +17610,4,118000060,1 +17611,5,118000070,1 +17612,1,101000000,2 +17613,3,101000006,2 +17614,3,103000005,2 +17615,4,101000003,2 +17616,4,102000005,2 +17617,4,107000005,2 +17618,4,112000006,2 +17619,5,101000009,2 +17620,5,101000011,2 +17621,5,101000012,2 +17622,5,101000013,2 +17623,5,101000014,2 +17624,5,101000015,2 +17625,5,101000016,2 +17626,5,101000017,2 +17627,5,101000018,2 +17628,5,102000008,2 +17629,5,102000009,2 +17630,5,107000007,2 +17631,5,109000008,2 +17632,5,111000007,2 +17633,5,111000008,2 +17634,5,112000008,2 +17635,5,120000008,2 +17636,3,110540,3 +17637,3,110680,3 +17638,3,110790,3 +17639,3,110800,3 +17640,3,120440,3 +17641,5,110055,3 +17642,5,110241,3 +17643,5,110251,3 +17644,5,110261,3 +17645,5,110271,3 +17646,5,110730,3 +17647,5,111020,3 +17648,5,111100,3 +17649,5,111160,3 +17650,5,120551,3 +17651,5,121160,3 \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index f5c114f..93d0589 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3,6 +3,7 @@ from datetime import datetime from construct import * import sys import csv +from csv import * class SaoBaseRequest: def __init__(self, data: bytes) -> None: @@ -1661,43 +1662,77 @@ class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): super().__init__(data) class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): - def __init__(self, cmd, randomized_unanalyzed_id) -> None: + def __init__(self, cmd, end_session_data) -> None: super().__init__(cmd) self.result = 1 - self.play_end_unanalyzed_log_reward_data_list_size = 1 # Number of arrays - self.unanalyzed_log_grade_id = 3 # RewardTable.csv - self.common_reward_data_size = 1 + self.unanalyzed_log_grade_id = [] - self.common_reward_type_1 = 1 - self.common_reward_id_1 = int(randomized_unanalyzed_id) - self.common_reward_num_1 = 1 + self.common_reward_type = [] + self.common_reward_id = [] + self.common_reward_num = 1 + + for x in range(len(end_session_data)): + self.common_reward_id.append(end_session_data[x]) + + with open('titles/sao/data/RewardTable.csv', 'r') as f: + keys_unanalyzed = next(f).strip().split(',') + data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + + for i in range(len(data_unanalyzed)): + if int(data_unanalyzed[i]["CommonRewardId"]) == int(end_session_data[x]): + self.unanalyzed_log_grade_id.append(int(data_unanalyzed[i]["UnanalyzedLogGradeId"])) + self.common_reward_type.append(int(data_unanalyzed[i]["CommonRewardType"])) + break + + self.unanalyzed_log_grade_id = list(map(int,self.unanalyzed_log_grade_id)) #int + self.common_reward_type = list(map(int,self.common_reward_type)) #int + self.common_reward_id = list(map(int,self.common_reward_id)) #int def make(self) -> bytes: + #new stuff + common_reward_data_struct = Struct( + "common_reward_type" / Int16ub, + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + ) + + play_end_unanalyzed_log_reward_data_list_struct = Struct( + "unanalyzed_log_grade_id" / Int32ub, + "common_reward_data_size" / Rebuild(Int32ub, len_(this.common_reward_data)), # big endian + "common_reward_data" / Array(this.common_reward_data_size, common_reward_data_struct), + ) + # create a resp struct resp_struct = Struct( "result" / Int8ul, # result is either 0 or 1 - "play_end_unanalyzed_log_reward_data_list_size" / Int32ub, # big endian - - "unanalyzed_log_grade_id" / Int32ub, - "common_reward_data_size" / Int32ub, - - "common_reward_type_1" / Int16ub, - "common_reward_id_1" / Int32ub, - "common_reward_num_1" / Int32ub, + "play_end_unanalyzed_log_reward_data_list_size" / Rebuild(Int32ub, len_(this.play_end_unanalyzed_log_reward_data_list)), # big endian + "play_end_unanalyzed_log_reward_data_list" / Array(this.play_end_unanalyzed_log_reward_data_list_size, play_end_unanalyzed_log_reward_data_list_struct), ) - resp_data = resp_struct.build(dict( + resp_data = resp_struct.parse(resp_struct.build(dict( result=self.result, - play_end_unanalyzed_log_reward_data_list_size=self.play_end_unanalyzed_log_reward_data_list_size, - - unanalyzed_log_grade_id=self.unanalyzed_log_grade_id, - common_reward_data_size=self.common_reward_data_size, + play_end_unanalyzed_log_reward_data_list_size=0, + play_end_unanalyzed_log_reward_data_list=[], + ))) - common_reward_type_1=self.common_reward_type_1, - common_reward_id_1=self.common_reward_id_1, - common_reward_num_1=self.common_reward_num_1, - )) + for i in range(len(self.common_reward_id)): + reward_resp_data = dict( + unanalyzed_log_grade_id=self.unanalyzed_log_grade_id[i], + common_reward_data_size=0, + common_reward_data=[], + ) + + reward_resp_data["common_reward_data"].append(dict( + common_reward_type=self.common_reward_type[i], + common_reward_id=self.common_reward_id[i], + common_reward_num=self.common_reward_num, + )) + + resp_data.play_end_unanalyzed_log_reward_data_list.append(reward_resp_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) self.length = len(resp_data) return super().make() + resp_data @@ -1733,8 +1768,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): self.concurrent_destroying_num.append(quest_data[i][7]) # quest_scene_ex_bonus_user_data_list - self.achievement_flag = [[1, 1, 1],[1, 1, 1]] - self.ex_bonus_table_id = [[1, 2, 3],[4, 5, 6]] + self.achievement_flag = [1,1,1] + self.ex_bonus_table_id = [1,2,3] self.quest_type = list(map(int,self.quest_type)) #int @@ -1748,8 +1783,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def make(self) -> bytes: #new stuff quest_scene_ex_bonus_user_data_list_struct = Struct( - "achievement_flag" / Int32ub, # big endian "ex_bonus_table_id" / Int32ub, # big endian + "achievement_flag" / Int8ul, # result is either 0 or 1 ) quest_scene_best_score_user_data_struct = Struct( @@ -1802,7 +1837,6 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): total_damage=[ord(x) for x in self.total_damage[i]], concurrent_destroying_num=self.concurrent_destroying_num[i], )) - resp_data.quest_scene_user_data_list.append(quest_resp_data) @@ -2462,7 +2496,7 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data - + class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) diff --git a/titles/sao/index.py b/titles/sao/index.py index 69a3db5..1fef504 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -112,5 +112,4 @@ class SaoServlet(resource.Resource): self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") - self.logger.debug(f"Response: {handler(sao_request).hex()}") return handler(sao_request) \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 8b46723..11adf27 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -1,6 +1,6 @@ from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select, update, delete from sqlalchemy.engine import Row @@ -122,6 +122,22 @@ sessions = Table( mysql_charset="utf8mb4", ) +end_sessions = Table( + "sao_end_sessions", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("quest_id", Integer, nullable=False), + Column("play_result_flag", Boolean, nullable=False), + Column("reward_data", JSON, nullable=True), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + mysql_charset="utf8mb4", +) + class SaoItemData(BaseData): def create_session(self, user_id: int, user_party_team_id: int, episode_id: int, play_mode: int, quest_drop_boost_apply_flag: int) -> Optional[int]: sql = insert(sessions).values( @@ -140,6 +156,22 @@ class SaoItemData(BaseData): return None return result.lastrowid + def create_end_session(self, user_id: int, quest_id: int, play_result_flag: bool, reward_data: JSON) -> Optional[int]: + sql = insert(end_sessions).values( + user=user_id, + quest_id=quest_id, + play_result_flag=play_result_flag, + reward_data=reward_data, + ) + + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO end session for user {user_id}!") + return None + return result.lastrowid + def put_item(self, user_id: int, item_id: int) -> Optional[int]: sql = insert(item_data).values( user=user_id, @@ -418,6 +450,22 @@ class SaoItemData(BaseData): return None return result.fetchone() + def get_end_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = end_sessions.select( + and_( + end_sessions.c.user == user_id, + ) + ).order_by( + end_sessions.c.play_date.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: sql = hero_log_data.delete( and_( From da422e602b4e6a0732523094e58d3c4d34a530e0 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 2 Jul 2023 15:50:42 -0400 Subject: [PATCH 297/589] fixing trial_tower_play_end_unanalyzed_log_fixed for SAO --- titles/sao/base.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 74aa85a..3c4aade 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1208,32 +1208,9 @@ class SaoBase: req_data = req_struct.parse(req) user_id = req_data.user_id - with open('titles/sao/data/RewardTable.csv', 'r') as f: - keys_unanalyzed = next(f).strip().split(',') - data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + end_session_data = self.game_data.item.get_end_session(user_id) - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - i = 0 - - # Create a loop to check if the id is a hero or else try 15 times before closing the loop and sending a dummy hero - while not heroList: - if i == 15: - # Return the dummy hero but not save it - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, 102000070) - return resp.make() - - i += 1 - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - - hero_data = self.game_data.item.get_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId']) - - # Avoid having a duplicated card and cause an overwrite - if not hero_data: - self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, randomized_unanalyzed_id['CommonRewardId']) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) return resp.make() def handle_cd00(self, request: Any) -> bytes: From d60f8270009d9e53a0fe215703ef5a11649f7c5e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 5 Jul 2023 10:47:43 -0400 Subject: [PATCH 298/589] fix typing across multiple games, fixes #23 --- titles/chuni/base.py | 4 ++-- titles/chuni/schema/item.py | 2 +- titles/cxb/base.py | 4 ++-- titles/mai2/base.py | 4 ++-- titles/mai2/dx.py | 2 +- titles/mai2/universe.py | 2 +- titles/ongeki/base.py | 4 ++-- titles/wacca/handlers/advertise.py | 18 +++++++++--------- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 689c2fe..ed8d0fb 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from time import strftime import pytz -from typing import Dict, Any +from typing import Dict, Any, List from core.config import CoreConfig from titles.chuni.const import ChuniConstants @@ -401,7 +401,7 @@ class ChuniBase: "userItemList": [], } - items: list[Dict[str, Any]] = [] + items: List[Dict[str, Any]] = [] for i in range(next_idx, len(user_item_list)): tmp = user_item_list[i]._asdict() tmp.pop("user") diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 94c4fd8..47ff633 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -296,7 +296,7 @@ class ChuniItemData(BaseData): self, version: int, room_id: int, - matching_member_info_list: list, + matching_member_info_list: List, user_id: int = None, rest_sec: int = 60, is_full: bool = False diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 6b6a5d5..749e8ac 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -2,7 +2,7 @@ import logging import json from decimal import Decimal from base64 import b64encode -from typing import Any, Dict +from typing import Any, Dict, List from hashlib import md5 from datetime import datetime @@ -416,7 +416,7 @@ class CxbBase: self.logger.info(f"Get best rankings for {uid}") p = self.data.score.get_best_rankings(uid) - rankList: list[Dict[str, Any]] = [] + rankList: List[Dict[str, Any]] = [] for rank in p: if rank["song_id"] is not None: diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 9f1ffaf..ad9aafd 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,5 +1,5 @@ from datetime import datetime, date, timedelta -from typing import Any, Dict +from typing import Any, Dict, List import logging from core.config import CoreConfig @@ -466,7 +466,7 @@ class Mai2Base: next_idx = int(data["nextIndex"] % 10000000000) user_item_list = self.data.item.get_items(data["userId"], kind) - items: list[Dict[str, Any]] = [] + items: List[Dict[str, Any]] = [] for i in range(next_idx, len(user_item_list)): tmp = user_item_list[i]._asdict() tmp.pop("user") diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 266332e..971135d 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -320,7 +320,7 @@ class Mai2DX(Mai2Base): next_idx = int(data["nextIndex"] % 10000000000) user_item_list = self.data.item.get_items(data["userId"], kind) - items: list[Dict[str, Any]] = [] + items: List[Dict[str, Any]] = [] for i in range(next_idx, len(user_item_list)): tmp = user_item_list[i]._asdict() tmp.pop("user") diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index f5b2515..f57ae1e 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -180,7 +180,7 @@ class Mai2Universe(Mai2DX): extend = extend._asdict() # parse the selectedCardList # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) - selected_cards: list = extend["selectedCardList"] + selected_cards: List = extend["selectedCardList"] # if no pass is already added, add the corresponding pass if not user_card["cardTypeId"] in selected_cards: diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 4f7619c..f10353c 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -268,7 +268,7 @@ class OngekiBase: } 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): @@ -443,7 +443,7 @@ class OngekiBase: "userItemList": [], } - items: list[Dict[str, Any]] = [] + items: List[Dict[str, Any]] = [] for i in range(data["nextIndex"] % 10000000000, len(p)): if len(items) > data["maxCount"]: break diff --git a/titles/wacca/handlers/advertise.py b/titles/wacca/handlers/advertise.py index 56186eb..47c8406 100644 --- a/titles/wacca/handlers/advertise.py +++ b/titles/wacca/handlers/advertise.py @@ -8,12 +8,12 @@ from titles.wacca.handlers.helpers import Notice class GetNewsResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() - self.notices: list[Notice] = [] - self.copywrightListings: list[str] = [] - self.stoppedSongs: list[int] = [] - self.stoppedJackets: list[int] = [] - self.stoppedMovies: list[int] = [] - self.stoppedIcons: list[int] = [] + self.notices: List[Notice] = [] + self.copywrightListings: List[str] = [] + self.stoppedSongs: List[int] = [] + self.stoppedJackets: List[int] = [] + self.stoppedMovies: List[int] = [] + self.stoppedIcons: List[int] = [] def make(self) -> Dict: note = [] @@ -34,7 +34,7 @@ class GetNewsResponseV1(BaseResponse): class GetNewsResponseV2(GetNewsResponseV1): - stoppedProducts: list[int] = [] + stoppedProducts: List[int] = [] def make(self) -> Dict: super().make() @@ -44,8 +44,8 @@ class GetNewsResponseV2(GetNewsResponseV1): class GetNewsResponseV3(GetNewsResponseV2): - stoppedNavs: list[int] = [] - stoppedNavVoices: list[int] = [] + stoppedNavs: List[int] = [] + stoppedNavVoices: List[int] = [] def make(self) -> Dict: super().make() From 1edec7dba27576ea1caf6aff43d92c4721f52a71 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 5 Jul 2023 12:35:00 -0400 Subject: [PATCH 299/589] sao: add response debug logging --- titles/sao/index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 1fef504..ef0bf89 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -112,4 +112,6 @@ class SaoServlet(resource.Resource): self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") - return handler(sao_request) \ No newline at end of file + resp = handler(sao_request) + self.logger.debug(f"Response: {resp.hex()}") + return resp \ No newline at end of file From 737312ca3dc7c643e5f10840a4a4826a58236045 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 7 Jul 2023 21:50:24 -0400 Subject: [PATCH 300/589] Threading support to main twisted reactor --- index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.py b/index.py index e936f14..70f4658 100644 --- a/index.py +++ b/index.py @@ -11,7 +11,7 @@ from twisted.web import server, resource from twisted.internet import reactor, endpoints from twisted.web.http import Request from routes import Mapper - +from threading import Thread class HttpDispatcher(resource.Resource): def __init__(self, cfg: CoreConfig, config_dir: str): @@ -283,4 +283,4 @@ if __name__ == "__main__": server.Site(dispatcher) ) - reactor.run() # type: ignore + Thread(target=reactor.run, args=(False,)).start() From 6c155a5e489791858f298f94c48929ce93db0c8c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jul 2023 00:01:52 -0400 Subject: [PATCH 301/589] database: add static variables to prevent having multiple sessions --- core/data/database.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index ffbefc0..1688812 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -15,6 +15,13 @@ from core.utils import Utils class Data: + current_schema_version = 4 + engine = None + session = None + user = None + arcade = None + card = None + base = None def __init__(self, cfg: CoreConfig) -> None: self.config = cfg @@ -24,22 +31,32 @@ class Data: 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) + if Data.engine is None: + Data.engine = create_engine(self.__url, pool_recycle=3600) + self.__engine = Data.engine - self.user = UserData(self.config, self.session) - self.arcade = ArcadeData(self.config, self.session) - self.card = CardData(self.config, self.session) - self.base = BaseData(self.config, self.session) - self.current_schema_version = 4 + if Data.session is None: + s = sessionmaker(bind=Data.engine, autoflush=True, autocommit=True) + Data.session = scoped_session(s) + + if Data.user is None: + Data.user = UserData(self.config, self.session) + + if Data.arcade is None: + Data.arcade = ArcadeData(self.config, self.session) + + if Data.card is None: + Data.card = CardData(self.config, self.session) + + if Data.base is None: + Data.base = BaseData(self.config, self.session) - log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("database") # Prevent the logger from adding handlers multiple times - if not getattr(self.logger, "handler_set", None): + if not getattr(self.logger, "handler_set", None): + log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) fileHandler = TimedRotatingFileHandler( "{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8", From 03cf535ff6006a1bf3eb6c0169dafcedc7a32877 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jul 2023 00:34:55 -0400 Subject: [PATCH 302/589] make threading optional --- core/config.py | 6 ++++++ docs/config.md | 1 + example_config/core.yaml | 1 + index.py | 5 ++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index 3fb0dbe..8b85353 100644 --- a/core/config.py +++ b/core/config.py @@ -36,6 +36,12 @@ class ServerConfig: self.__config, "core", "server", "is_develop", default=True ) + @property + def threading(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "server", "threading", default=False + ) + @property def log_dir(self) -> str: return CoreConfig.get_config_field( diff --git a/docs/config.md b/docs/config.md index 3e4ba8a..18f90eb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -5,6 +5,7 @@ - `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` - `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` +- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False` - `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` ## Title - `loglevel`: Logging level for the title server. Default `info` diff --git a/example_config/core.yaml b/example_config/core.yaml index 382c51b..ebf99f1 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -4,6 +4,7 @@ server: allow_unregistered_serials: True name: "ARTEMiS" is_develop: True + threading: False log_dir: "logs" title: diff --git a/index.py b/index.py index 70f4658..15d6866 100644 --- a/index.py +++ b/index.py @@ -283,4 +283,7 @@ if __name__ == "__main__": server.Site(dispatcher) ) - Thread(target=reactor.run, args=(False,)).start() + if cfg.server.threading: + Thread(target=reactor.run, args=(False,)).start() + else: + reactor.run() From 09c4f8cda43e2d63f4d19e42393db6b2e604520e Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 8 Jul 2023 18:44:02 -0400 Subject: [PATCH 303/589] Async request to CXB profile loading --- titles/cxb/base.py | 299 +++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 135 deletions(-) diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 749e8ac..a649f31 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -11,6 +11,7 @@ from titles.cxb.config import CxbConfig from titles.cxb.const import CxbConstants from titles.cxb.database import CxbData +from threading import Thread class CxbBase: def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: @@ -54,6 +55,151 @@ class CxbBase: self.logger.warn(f"User {data['login']['authid']} does not have a profile") return {} + def task_generateCoupon(index, data1): + # Coupons + 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") + ) + + def task_generateShopListTitle(index, data1): + # ShopList_Title + 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") + ) + + def task_generateShopListIcon(index, data1): + # 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") + ) + + def task_generateStories(index, data1): + # 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") + ) + + def task_generateScoreData(song, index, data1): + 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") + ) + + def task_generateIndexData(versionindex): + try: + v_profile = self.data.profile.get_profile_index(0, uid, self.version) + v_profile_data = v_profile["data"] + versionindex.append(int(v_profile_data["appVersion"])) + except: + versionindex.append("10400") + def handle_action_loadrange_request(self, data: Dict) -> Dict: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] @@ -107,146 +253,29 @@ class CxbBase: 900000 = Stories """ - # Coupons - 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") - ) + # Async threads to generate the response + thread_Coupon = Thread(target=CxbBase.task_generateCoupon(index, data1)) + thread_ShopListTitle = Thread(target=CxbBase.task_generateShopListTitle(index, data1)) + thread_ShopListIcon = Thread(target=CxbBase.task_generateShopListIcon(index, data1)) + thread_Stories = Thread(target=CxbBase.task_generateStories(index, data1)) - # ShopList_Title - 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") - ) + thread_Coupon.start() + thread_ShopListTitle.start() + thread_ShopListIcon.start() + thread_Stories.start() - # 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") - ) - - # 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") - ) + thread_Coupon.join() + thread_ShopListTitle.join() + thread_ShopListIcon.join() + thread_Stories.join() 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") - ) + thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) + thread_ScoreData.start() for v in index: - try: - v_profile = self.data.profile.get_profile_index(0, uid, self.version) - v_profile_data = v_profile["data"] - versionindex.append(int(v_profile_data["appVersion"])) - except: - versionindex.append("10400") + thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex)) + thread_IndexData.start() return {"index": index, "data": data1, "version": versionindex} @@ -544,4 +573,4 @@ class CxbBase: def handle_action_stampreq_request(self, data: Dict) -> Dict: self.logger.info(data) - return {"stampreq": ""} + return {"stampreq": ""} \ No newline at end of file From 75842b5a886b3f61fdf50c361894a9887af39618 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:12:34 +0000 Subject: [PATCH 304/589] Add team support, implement team upsert, add rivals --- titles/chuni/base.py | 141 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 10 deletions(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index ed8d0fb..b9c1ae8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -361,11 +361,98 @@ class ChuniBase: "userDuelList": duel_list, } + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_rival(data["rivalId"]) + if p is None: + return {} + userRivalData = { + "rivalId": p.user, + "rivalName": p.userName + } + return { + "userId": data["userId"], + "userRivalData": userRivalData + } + + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) + if m is None: + return {} + + user_rival_music_list = [] + for music in m: + actual_music_id = self.data.static.get_song(music["musicId"]) + if actual_music_id is None: + music_id = music["musicId"] + else: + music_id = actual_music_id["songId"] + level = music["level"] + score = music["score"] + rank = music["rank"] + + # Find the existing entry for the current musicId in the user_rival_music_list + music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None) + + if music_entry is None: + # If the entry doesn't exist, create a new entry + music_entry = { + "musicId": music_id, + "length": 0, + "userRivalMusicDetailList": [] + } + user_rival_music_list.append(music_entry) + + # Check if the current score is higher than the previous highest score for the level + level_entry = next( + ( + entry + for entry in music_entry["userRivalMusicDetailList"] + if entry["level"] == level + ), + None, + ) + if level_entry is None or score > level_entry["scoreMax"]: + # If the level entry doesn't exist or the score is higher, update or add the entry + level_entry = { + "level": level, + "scoreMax": score, + "scoreRank": rank + } + + if level_entry not in music_entry["userRivalMusicDetailList"]: + music_entry["userRivalMusicDetailList"].append(level_entry) + + music_entry["length"] = len(music_entry["userRivalMusicDetailList"]) + + result = { + "userId": data["userId"], + "rivalId": data["rivalId"], + "nextIndex": -1, + "userRivalMusicList": user_rival_music_list + } + + return result + def handle_get_user_rival_music_api_requestded(self, data: Dict) -> Dict: + m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) + if m is None: + return {} + + userRivalMusicList = [] + for music in m: + self.logger.debug(music["point"]) + + return { + "userId": data["userId"], + "rivalId": data["rivalId"], + "nextIndex": -1 + + } + def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] # still needs to be implemented on WebUI - # 1: Music, 3: Character + # 1: Music, 2: User, 3: Character fav_list = self.data.item.get_all_favorites( data["userId"], self.version, fav_kind=int(data["kind"]) ) @@ -600,25 +687,43 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: use the database "chuni_profile_team" with a GUI + # Default values + team_id = 65535 team_name = self.game_cfg.team.team_name - if team_name == "": + team_rank = 0 + + # Get user profile + profile = self.data.profile.get_profile_data(data["userId"], self.version) + if profile and profile["teamId"]: + # Get team by id + team = self.data.profile.get_team_by_id(profile["teamId"]) + + if team: + team_id = team["id"] + team_name = team["teamName"] + # Determine whether to use scaled ranks, or original system + if self.game_cfg.team.rank_scale: + team_rank = self.data.profile.get_team_rank(team["id"]) + else: + team_rank = self.data.profile.get_team_rank_actual(team["id"]) + + # Don't return anything if no team name has been defined for defaults and there is no team set for the player + if not profile["teamId"] and team_name == "": return {"userId": data["userId"], "teamId": 0} return { "userId": data["userId"], - "teamId": 1, - "teamRank": 1, + "teamId": team_id, + "teamRank": team_rank, "teamName": team_name, "userTeamPoint": { "userId": data["userId"], - "teamId": 1, + "teamId": team_id, "orderId": 1, "teamPoint": 1, "aggrDate": data["playDate"], }, } - def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], @@ -709,9 +814,25 @@ class ChuniBase: self.data.score.put_playlog(user_id, playlog) if "userTeamPoint" in upsert: - # TODO: team stuff - pass + team_points = upsert["userTeamPoint"] + try: + for tp in team_points: + if tp["teamId"] != '65535': + # Fetch the current team data + current_team = self.data.profile.get_team_by_id(tp["teamId"]) + # Calculate the new teamPoint + new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"] + + # Prepare the data to update + team_data = { + "teamPoint": new_team_point + } + + # Update the team data + self.data.profile.update_team(tp["teamId"], team_data) + except: + pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: self.data.item.put_map_area(user_id, map_area) @@ -757,4 +878,4 @@ class ChuniBase: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } + } \ No newline at end of file From c01d3f49f57b650485f82526a4ec12d8594efe3c Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:13:19 +0000 Subject: [PATCH 305/589] Added call for getting rival's music lists --- titles/chuni/schema/score.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 203aa11..ab26f5f 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -200,3 +200,10 @@ class ChuniScoreData(BaseData): if result is None: return None return result.lastrowid + + def get_rival_music(self, rival_id: int, index: int, max_count: int) -> Optional[List[Dict]]: + sql = select(playlog).where(playlog.c.user == rival_id).limit(max_count).offset(index) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() \ No newline at end of file From 043ff1700810ea5c6c863c09db0fc40eed161b99 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:14:53 +0000 Subject: [PATCH 306/589] Add team support, rivals, and test function for getting playcounts --- titles/chuni/schema/profile.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index f8edc33..b055fcb 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -637,3 +637,103 @@ class ChuniProfileData(BaseData): if result is None: return None return result.fetchall() + + def get_team_by_id(self, team_id: int) -> Optional[Row]: + sql = select(team).where(team.c.id == team_id) + result = self.execute(sql) + + if result is None: + return None + return result.fetchone() + + def get_team_rank_actual(self, team_id: int) -> int: + # Normal ranking system, likely the one used in the real servers + # Query all teams sorted by 'teamPoint' + result = self.execute( + select(team.c.id).order_by(team.c.teamPoint.desc()) + ) + + # Get the rank of the team with the given team_id + rank = None + for i, row in enumerate(result, start=1): + if row.id == team_id: + rank = i + break + + # Return the rank if found, or a default rank otherwise + return rank if rank is not None else 0 + + def get_team_rank(self, team_id: int) -> int: + # Scaled ranking system, designed for smaller instances. + # Query all teams sorted by 'teamPoint' + result = self.execute( + select(team.c.id).order_by(team.c.teamPoint.desc()) + ) + + # Count total number of teams + total_teams = self.execute(select(func.count()).select_from(team)).scalar() + + # Get the rank of the team with the given team_id + rank = None + for i, row in enumerate(result, start=1): + if row.id == team_id: + rank = i + break + + # If the team is not found, return default rank + if rank is None: + return 0 + + # Define rank tiers + tiers = { + 1: range(1, int(total_teams * 0.1) + 1), # Rainbow + 2: range(int(total_teams * 0.1) + 1, int(total_teams * 0.4) + 1), # Gold + 3: range(int(total_teams * 0.4) + 1, int(total_teams * 0.7) + 1), # Silver + 4: range(int(total_teams * 0.7) + 1, total_teams + 1), # Grey + } + + # Assign rank based on tier + for tier_rank, tier_range in tiers.items(): + if rank in tier_range: + return tier_rank + + # Return default rank if not found in any tier + return 0 + + def update_team(self, team_id: int, team_data: Dict) -> bool: + team_data["id"] = team_id + + sql = insert(team).values(**team_data) + conflict = sql.on_duplicate_key_update(**team_data) + + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"update_team: Failed to update team! team id: {team_id}" + ) + return False + return True + def get_rival(self, rival_id: int) -> Optional[Row]: + sql = select(profile).where(profile.c.user == rival_id) + result = self.execute(sql) + + if result is None: + return None + return result.fetchone() + def get_overview(self) -> Dict: + # Fetch and add up all the playcounts + playcount_sql = self.execute(select(profile.c.playCount)) + + if playcount_sql is None: + self.logger.warn( + f"get_overview: Couldn't pull playcounts" + ) + return 0 + + total_play_count = 0; + for row in playcount_sql: + total_play_count += row[0] + return { + "total_play_count": total_play_count + } \ No newline at end of file From b42e8ab76c8bb6bfdf671aa013e3afc034ea9ff9 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:16:11 +0000 Subject: [PATCH 307/589] Added function for pulling a song via the DB unique ID instead of the native song ID --- titles/chuni/schema/static.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 85d0397..3796232 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -453,6 +453,15 @@ class ChuniStaticData(BaseData): return None return result.fetchone() + def get_song(self, music_id: int) -> Optional[Row]: + sql = music.select(music.c.id == music_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_avatar( self, version: int, @@ -587,4 +596,4 @@ class ChuniStaticData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchone() \ No newline at end of file From eecd3a829dcc029dcf6e472e68ecc5f8aaa91417 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:40:49 +0000 Subject: [PATCH 308/589] Added value for team rank scaling, and documented it a bit --- example_config/chuni.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 59db51e..ed0aca0 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -3,7 +3,8 @@ server: loglevel: "info" team: - name: ARTEMiS + name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. + rankScale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses. mods: use_login_bonus: True From 1bc8648e357bc75a8782799e5c57b98041846b90 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:56:09 +0000 Subject: [PATCH 309/589] I knew I forgot something (fixed config) --- example_config/chuni.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index ed0aca0..687b195 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -4,7 +4,7 @@ server: team: name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. - rankScale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses. + rank_scale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses. mods: use_login_bonus: True From 3c7ceabf4e1ac8e07bd1dac8d932a2539bc57771 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 10:04:25 +0000 Subject: [PATCH 310/589] And again --- titles/chuni/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 48d70d2..9b294ad 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -30,6 +30,11 @@ class ChuniTeamConfig: return CoreConfig.get_config_field( self.__config, "chuni", "team", "name", default="" ) + @property + def rank_scale(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "rank_scale", default="False" + ) class ChuniModsConfig: From 85b73e634d64c5d2408a8b6b77399b1df25f31bf Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 12 Jul 2023 00:41:53 -0400 Subject: [PATCH 311/589] mucha: add DownloadState --- core/mucha.py | 81 ++++++++++++++++++++++++++++++++++++++++++--------- index.py | 7 +++++ 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/core/mucha.py b/core/mucha.py index eecae3a..6bb34d7 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -4,6 +4,7 @@ from logging.handlers import TimedRotatingFileHandler from twisted.web import resource from twisted.web.http import Request from datetime import datetime +from Crypto.Cipher import Blowfish import pytz from core import CoreConfig @@ -56,18 +57,22 @@ class MuchaServlet: self.logger.error( f"Error processing mucha request {request.content.getvalue()}" ) - return b"" + return b"RESULTS=000" req = MuchaAuthRequest(req_dict) + self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") - return b"" + return b"RESULTS=000" # TODO: Decrypt S/N + #cipher = Blowfish.new(req.sendDate.encode(), Blowfish.MODE_ECB) + #sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum)) + #self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") + resp = MuchaAuthResponse( f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" ) @@ -84,22 +89,37 @@ class MuchaServlet: self.logger.error( f"Error processing mucha request {request.content.getvalue()}" ) - return b"" + return b"RESULTS=000" req = MuchaUpdateRequest(req_dict) + self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") - return b"" + return b"RESULTS=000" - resp = MuchaUpdateResponseStub(req.gameVer) + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}") self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) + def handle_dlstate(self, request: Request, _: Dict) -> bytes: + req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + + if req_dict is None: + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"" + + req = MuchaDownloadStateRequest(req_dict) + self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}") + self.logger.debug(f"request {vars(req)}") + return b"RESULTS=001" + def mucha_preprocess(self, data: bytes) -> Optional[Dict]: try: ret: Dict[str, Any] = {} @@ -202,22 +222,57 @@ class MuchaUpdateRequest: class MuchaUpdateResponse: def __init__(self, game_ver: str, mucha_url: str) -> None: - self.RESULTS = "001" + self.RESULTS = "001" + self.EXE_VER = game_ver + self.UPDATE_VER_1 = game_ver - self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/" - self.UPDATE_SIZE_1 = "0" - self.UPDATE_CRC_1 = "0000000000000000" - self.CHECK_URL_1 = f"https://{mucha_url}/checkUrl/" - self.EXE_VER_1 = game_ver + self.UPDATE_URL_1 = f"http://{mucha_url}/updUrl1/" + self.UPDATE_SIZE_1 = "20" + + self.CHECK_CRC_1 = "0000000000000000" + self.CHECK_URL_1 = f"http://{mucha_url}/checkUrl/" + self.CHECK_SIZE_1 = "20" + self.INFO_SIZE_1 = "0" self.COM_SIZE_1 = "0" self.COM_TIME_1 = "0" self.LAN_INFO_SIZE_1 = "0" + self.USER_ID = "" self.PASSWORD = "" +""" +RESULTS +EXE_VER +UPDATE_VER_%d +UPDATE_URL_%d +UPDATE_SIZE_%d + +CHECK_CRC_%d +CHECK_URL_%d +CHECK_SIZE_%d + +INFO_SIZE_1 +COM_SIZE_1 +COM_TIME_1 +LAN_INFO_SIZE_1 + +USER_ID +PASSWORD +""" class MuchaUpdateResponseStub: def __init__(self, game_ver: str) -> None: self.RESULTS = "001" self.UPDATE_VER_1 = game_ver + +class MuchaDownloadStateRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.updateVer = request.get("updateVer", "") + self.serialNum = request.get("serialNum", "") + self.fileSize = request.get("fileSize", "") + self.compFileSize = request.get("compFileSize", "") + self.boardId = request.get("boardId", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") diff --git a/index.py b/index.py index 15d6866..2ff7c04 100644 --- a/index.py +++ b/index.py @@ -113,6 +113,13 @@ class HttpDispatcher(resource.Resource): action="handle_updatecheck", conditions=dict(method=["POST"]), ) + self.map_post.connect( + "mucha_dlstate", + "/mucha/downloadstate.do", + controller="mucha", + action="handle_dlstate", + conditions=dict(method=["POST"]), + ) self.map_get.connect( "title_get", From 6a41dac46cf1196f310b03ada96045bd38c032fa Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 12 Jul 2023 11:25:46 +0200 Subject: [PATCH 312/589] ongeki: card maker config added, small fixes, improved credits - Changed the credits config to the default 370 instead of 360 - Added `start_date` to the events to show new events - Fixed Card Maker Gachas to only allow "Select Gacha" once - Fixed the `get_profile_rating_log` database query --- core/data/schema/versions/SDDT_4_rollback.sql | 2 ++ core/data/schema/versions/SDDT_5_upgrade.sql | 2 ++ docs/game_specific_info.md | 26 ++++++++++++------- example_config/ongeki.yaml | 6 +++++ readme.md | 2 +- titles/ongeki/__init__.py | 2 +- titles/ongeki/base.py | 18 +++++++++---- titles/ongeki/bright.py | 19 +++++++++----- titles/ongeki/brightmemory.py | 11 -------- titles/ongeki/config.py | 17 ++++++++++++ titles/ongeki/const.py | 14 +++++----- titles/ongeki/index.py | 9 +++++-- titles/ongeki/schema/profile.py | 2 +- titles/ongeki/schema/static.py | 1 + 14 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 core/data/schema/versions/SDDT_4_rollback.sql create mode 100644 core/data/schema/versions/SDDT_5_upgrade.sql diff --git a/core/data/schema/versions/SDDT_4_rollback.sql b/core/data/schema/versions/SDDT_4_rollback.sql new file mode 100644 index 0000000..fdd369b --- /dev/null +++ b/core/data/schema/versions/SDDT_4_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE ongeki_static_events +DROP COLUMN startDate; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_5_upgrade.sql b/core/data/schema/versions/SDDT_5_upgrade.sql new file mode 100644 index 0000000..6d4b421 --- /dev/null +++ b/core/data/schema/versions/SDDT_5_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE ongeki_static_events +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index d5d1eff..e2a8296 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -230,12 +230,12 @@ python dbutils.py --game SBZV upgrade |------------|----------------------------| | 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 | +| 2 | O.N.G.E.K.I. SUMMER | +| 3 | O.N.G.E.K.I. SUMMER + | +| 4 | O.N.G.E.K.I. R.E.D. | +| 5 | O.N.G.E.K.I. R.E.D. + | +| 6 | O.N.G.E.K.I. bright | +| 7 | O.N.G.E.K.I. bright MEMORY | ### Importer @@ -285,12 +285,12 @@ python dbutils.py --game SDDT upgrade * Card Maker 1.30: * CHUNITHM NEW!!: Yes * maimai DX UNiVERSE: Yes - * O.N.G.E.K.I. Bright: Yes + * O.N.G.E.K.I. bright: Yes * Card Maker 1.35: * CHUNITHM SUN: Yes (NEW PLUS!! up to A032) * maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031) - * O.N.G.E.K.I. Bright Memory: Yes + * O.N.G.E.K.I. bright MEMORY: Yes ### Importer @@ -345,7 +345,15 @@ Now update your `config/cardmaker.yaml` with the correct version number, for exa version: 1: # Card Maker 1.35 ongeki: 1.35.03 -``` +``` + +For now you also need to update your `config/ongeki.yaml` with the correct version number, for example: + +```yaml +version: + 7: # O.N.G.E.K.I. bright MEMORY + card_maker: 1.35.03 +``` ### O.N.G.E.K.I. diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index 3db7098..90233b3 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -29,3 +29,9 @@ gachas: - 1156 - 1163 - 1164 + +version: + 6: + card_maker: 1.30.01 + 7: + card_maker: 1.35.03 diff --git a/readme.md b/readme.md index 224fa5b..2c49faa 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + 1.35 + O.N.G.E.K.I. - + All versions up to Bright Memory + + All versions up to bright MEMORY + WACCA + Lily R diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 1ba901b..b887ba6 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 = 4 +current_schema_version = 5 diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index f10353c..ace1d12 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -142,7 +142,7 @@ class OngekiBase: def handle_get_game_point_api_request(self, data: Dict) -> Dict: """ - Sets the GP ammount for A and B sets for 1 - 3 crdits + Sets the GP amount for A and B sets for 1 - 3 credits """ return { "length": 6, @@ -155,13 +155,13 @@ class OngekiBase: }, { "type": 1, - "cost": 200, + "cost": 230, "startDate": "2000-01-01 05:00:00.0", "endDate": "2099-01-01 05:00:00.0", }, { "type": 2, - "cost": 300, + "cost": 370, "startDate": "2000-01-01 05:00:00.0", "endDate": "2099-01-01 05:00:00.0", }, @@ -256,7 +256,11 @@ class OngekiBase: { "type": event["type"], "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + event["startDate"], "%Y-%m-%d %H:%M:%S.0" + ), "endDate": "2099-12-31 00:00:00.0", } ) @@ -560,7 +564,11 @@ 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 {} + return { + "userId": data["userId"], + "length": 0, + "userRecentRatingList": [], + } userRecentRatingList = recent_rating["recentRating"] diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 06155a1..49d6216 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -43,15 +43,15 @@ class OngekiBright(OngekiBase): 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 - # Card Maker 1.34.00 = 1.30.01 - # Card Maker 1.36.00 = 1.35.04 - user_data["compatibleCmVersion"] = "1.30.01" + # add the compatible card maker version from config + card_maker_ver = self.game_cfg.version.version(self.version) + if card_maker_ver and card_maker_ver.get("card_maker"): + # Card Maker 1.30 = 1.30.01+ + # Card Maker 1.35 = 1.35.03+ + user_data["compatibleCmVersion"] = card_maker_ver.get("card_maker") return {"userId": data["userId"], "userData": user_data} @@ -333,6 +333,8 @@ class OngekiBright(OngekiBase): select_point = data["selectPoint"] total_gacha_count, ceiling_gacha_count = 0, 0 + # 0 = can still use Gacha Select, 1 = already used Gacha Select + use_select_point = 0 daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") @@ -344,6 +346,9 @@ class OngekiBright(OngekiBase): daily_gacha_cnt = user_gacha["dailyGachaCnt"] five_gacha_cnt = user_gacha["fiveGachaCnt"] eleven_gacha_cnt = user_gacha["elevenGachaCnt"] + # if the Gacha Select has been used, make sure to keep it + if user_gacha["useSelectPoint"] == 1: + use_select_point = 1 # parse just the year, month and date daily_gacha_date = user_gacha["dailyGachaDate"] @@ -359,7 +364,7 @@ class OngekiBright(OngekiBase): totalGachaCnt=total_gacha_count + gacha_count, ceilingGachaCnt=ceiling_gacha_count + gacha_count, selectPoint=select_point, - useSelectPoint=0, + useSelectPoint=use_select_point, dailyGachaCnt=daily_gacha_cnt + gacha_count, fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt, elevenGachaCnt=eleven_gacha_cnt + 1 diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index 6e2548b..d7103a3 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -136,14 +136,3 @@ class OngekiBrightMemory(OngekiBright): def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: return {"techScore": 0, "cardNum": 0} - - 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 = 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/config.py b/titles/ongeki/config.py index 1117b39..c20b1ed 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -1,3 +1,4 @@ +from ast import Dict from typing import List from core.config import CoreConfig @@ -33,7 +34,23 @@ class OngekiGachaConfig: ) +class OngekiCardMakerVersionConfig: + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + def version(self, version: int) -> Dict: + """ + in the form of: + : {"card_maker": } + 6: {"card_maker": 1.30.01} + """ + return CoreConfig.get_config_field( + self.__config, "ongeki", "version", default={} + ).get(version) + + class OngekiConfig(dict): def __init__(self) -> None: self.server = OngekiServerConfig(self) self.gachas = OngekiGachaConfig(self) + self.version = OngekiCardMakerVersionConfig(self) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index ceef317..8d4c4ab 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -66,13 +66,13 @@ class OngekiConstants: VERSION_NAMES = ( "ONGEKI", - "ONGEKI+", - "ONGEKI Summer", - "ONGEKI Summer+", - "ONGEKI Red", - "ONGEKI Red+", - "ONGEKI Bright", - "ONGEKI Bright Memory", + "ONGEKI +", + "ONGEKI SUMMER", + "ONGEKI SUMMER +", + "ONGEKI R.E.D.", + "ONGEKI R.E.D. +", + "ONGEKI bright", + "ONGEKI bright MEMORY", ) @classmethod diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 7927d84..af206e9 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -11,6 +11,7 @@ from os import path from typing import Tuple from core.config import CoreConfig +from core.utils import Utils from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants from titles.ongeki.base import OngekiBase @@ -101,6 +102,7 @@ class OngekiServlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 internal_ver = OngekiConstants.VER_ONGEKI @@ -137,7 +139,10 @@ class OngekiServlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -156,6 +161,6 @@ class OngekiServlet: if resp == None: resp = {"returnCode": 1} - self.logger.info(f"Response {resp}") + self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index a112bf2..374503e 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -316,7 +316,7 @@ class OngekiProfileData(BaseData): return result.fetchone() def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: - sql = select(rating_log).where(recent_rating.c.user == aime_id) + sql = select(rating_log).where(rating_log.c.user == aime_id) result = self.execute(sql) if result is None: diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index ed81ebf..b34802d 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -16,6 +16,7 @@ events = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), mysql_charset="utf8mb4", From 2f135968858f68a8e53a418806ff7c316ae6750f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Jul 2023 00:15:14 -0400 Subject: [PATCH 313/589] fix db ignoring port in config, createing database no longer runs over version --- core/data/database.py | 6 +++--- core/data/schema/base.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 1688812..9fb2606 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -27,9 +27,9 @@ class Data: if self.config.database.sha2_password: passwd = sha256(self.config.database.password.encode()).digest() - self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" + self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4" else: - self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" + self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4" if Data.engine is None: Data.engine = create_engine(self.__url, pool_recycle=3600) @@ -94,7 +94,7 @@ class Data: game_mod.database(self.config) metadata.create_all(self.__engine.connect()) - self.base.set_schema_ver( + self.base.touch_schema_ver( game_mod.current_schema_version, game_mod.game_codes[0] ) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 7957301..319101f 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -102,6 +102,18 @@ class BaseData: return None return row["version"] + + def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: + sql = insert(schema_ver).values(game=game, version=ver) + conflict = sql.on_duplicate_key_update(version=schema_ver.c.version) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"Failed to update schema version for game {game} (v{ver})" + ) + return None + return result.lastrowid def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: sql = insert(schema_ver).values(game=game, version=ver) From f39317301b9b892c5852258d4e15b770b38ba8d3 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sat, 15 Jul 2023 22:51:54 +0200 Subject: [PATCH 314/589] mai2: fixed update script, added mai2 heredity, fixed cards import --- core/data/schema/versions/SDEZ_5_rollback.sql | 2 +- core/data/schema/versions/SDEZ_6_upgrade.sql | 2 +- titles/cm/index.py | 2 +- titles/cm/read.py | 12 +- titles/mai2/dx.py | 173 ------------------ titles/mai2/festival.py | 6 +- titles/mai2/splash.py | 4 +- titles/mai2/splashplus.py | 4 +- titles/mai2/universe.py | 12 +- titles/mai2/universeplus.py | 4 +- 10 files changed, 26 insertions(+), 195 deletions(-) diff --git a/core/data/schema/versions/SDEZ_5_rollback.sql b/core/data/schema/versions/SDEZ_5_rollback.sql index 1507faa..2b66afe 100644 --- a/core/data/schema/versions/SDEZ_5_rollback.sql +++ b/core/data/schema/versions/SDEZ_5_rollback.sql @@ -51,7 +51,7 @@ ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL; -ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NOT NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL; diff --git a/core/data/schema/versions/SDEZ_6_upgrade.sql b/core/data/schema/versions/SDEZ_6_upgrade.sql index 06b2d45..472747d 100644 --- a/core/data/schema/versions/SDEZ_6_upgrade.sql +++ b/core/data/schema/versions/SDEZ_6_upgrade.sql @@ -35,7 +35,7 @@ ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL; -ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL; ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL; diff --git a/titles/cm/index.py b/titles/cm/index.py index 3bde49c..348ec4f 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -129,6 +129,6 @@ class CardMakerServlet: if resp is None: resp = {"returnCode": 1} - self.logger.info(f"Response {resp}") + self.logger.debug(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 index 109483c..a26f35e 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -89,8 +89,7 @@ class CardMakerReader(BaseReader): 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, + "v2_10": ChuniConstants.VER_CHUNITHM_SUN, } for root, dirs, files in os.walk(base_dir): @@ -138,8 +137,7 @@ class CardMakerReader(BaseReader): 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, + "v2_10": ChuniConstants.VER_CHUNITHM_SUN, } for root, dirs, files in os.walk(base_dir): @@ -226,6 +224,12 @@ class CardMakerReader(BaseReader): True if troot.find("disable").text == "false" else False ) + # check if a date is part of the name and disable the + # card if it is + enabled = ( + False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled + ) + self.mai2_data.static.put_card( version, card_id, name, enabled=enabled ) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 971135d..185dda0 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -568,176 +568,3 @@ class Mai2DX(Mai2Base): "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}], } - - 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/festival.py b/titles/mai2/festival.py index 85b3df2..fcdd4ed 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.dx import Mai2DX +from titles.mai2.universeplus import Mai2UniversePlus from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Festival(Mai2DX): +class Mai2Festival(Mai2UniversePlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL @@ -14,7 +14,7 @@ class Mai2Festival(Mai2DX): 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.36 + # hardcode lastDataVersion for CardMaker 1.35 user_data["lastDataVersion"] = "1.30.00" return user_data diff --git a/titles/mai2/splash.py b/titles/mai2/splash.py index 0c9f827..026e3ae 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.dx import Mai2DX +from titles.mai2.dxplus import Mai2DXPlus from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Splash(Mai2DX): +class Mai2Splash(Mai2DXPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index e26b267..438941f 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.dx import Mai2DX +from titles.mai2.splash import Mai2Splash from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2SplashPlus(Mai2DX): +class Mai2SplashPlus(Mai2Splash): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index f57ae1e..c17f899 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -5,12 +5,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.dx import Mai2DX +from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Universe(Mai2DX): +class Mai2Universe(Mai2SplashPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE @@ -70,13 +70,13 @@ class Mai2Universe(Mai2DX): 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["startDate"] = datetime.strftime(tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT) + tmp["endDate"] = datetime.strftime(tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT) tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT ) tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT ) selling_card_list.append(tmp) diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index e9f03f4..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.dx import Mai2DX +from titles.mai2.universe import Mai2Universe from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2DX): +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 From d0e43140ba162a2716c8c04187e64630b20c9ea1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 16:06:34 -0400 Subject: [PATCH 315/589] mai2: fix ghost saving, add memorial photo upload --- titles/mai2/base.py | 66 ++++++++++++++++++++++++++++++++++++++++++-- titles/mai2/dx.py | 12 +++++--- titles/mai2/index.py | 15 +++++++++- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ad9aafd..267edec 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,6 +1,8 @@ -from datetime import datetime, date, timedelta +from datetime import datetime from typing import Any, Dict, List import logging +from base64 import b64decode +from os import path, stat from core.config import CoreConfig from titles.mai2.const import Mai2Constants @@ -773,4 +775,64 @@ class Mai2Base: self.logger.debug(data) def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: - self.logger.debug(data) \ No newline at end of file + if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + photo = data.get("userPhoto", {}) + + if photo is None or not photo: + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + order_id = int(photo.get("orderId", -1)) + user_id = int(photo.get("userId", -1)) + div_num = int(photo.get("divNumber", -1)) + div_len = int(photo.get("divLength", -1)) + div_data = photo.get("divData", "") + playlog_id = int(photo.get("playlogId", -1)) + track_num = int(photo.get("trackNo", -1)) + upload_date = photo.get("uploadDate", "") + + if order_id < 0 or user_id <= 0 or div_num < 0 or div_len <= 0 or not div_data or playlog_id < 0 or track_num <= 0 or not upload_date: + self.logger.warn(f"Malformed photo upload request") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if order_id == 0 and div_num > 0: + self.logger.warn(f"Failed to set orderId properly (still 0 after first chunk)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_num == 0 and order_id > 0: + self.logger.warn(f"First chuck re-send, Ignore") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_num >= div_len: + self.logger.warn(f"Sent extra chunks ({div_num} >= {div_len})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_len >= 100: + self.logger.warn(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + photo_chunk = b64decode(div_data) + + if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len): + self.logger.warn(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if not path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") and div_num != 0: + self.logger.warn(f"Out of order photo upload (div_num {div_num})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") and div_num == 0: + self.logger.warn(f"Duplicate file upload") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + elif path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg"): + fstats = stat(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") + if fstats.st_size != 10240 * div_num: + self.logger.warn(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + with open(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg", "ab") as f: + f.write(photo_chunk) + + return {'returnCode': order_id + 1, 'apiName': 'UploadUserPhotoApi'} diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 185dda0..fba092a 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -117,7 +117,7 @@ class Mai2DX(Mai2Base): if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_extend(user_id, self.version, ghost) + self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_option( @@ -217,9 +217,6 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - def handle_user_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) if profile is None: @@ -568,3 +565,10 @@ class Mai2DX(Mai2Base): "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}], } + + def handle_user_login_api_request(self, data: Dict) -> Dict: + ret = super().handle_user_login_api_request(data) + if ret is None or not ret: + return ret + ret['loginId'] = ret.get('loginCount', 0) + return ret diff --git a/titles/mai2/index.py b/titles/mai2/index.py index c250894..b54832e 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -7,7 +7,7 @@ import string import logging, coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler -from os import path +from os import path, mkdir from typing import Tuple from core.config import CoreConfig @@ -109,6 +109,19 @@ class Mai2Servlet: f"{core_cfg.title.hostname}", ) + def setup(self): + if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir): + try: + mkdir(self.game_cfg.uploads.photos_dir) + except: + self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}") + + if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir): + try: + mkdir(self.game_cfg.uploads.movies_dir) + except: + self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') From 343fe4357cac314ed7625a1a2b92fffc04f4971d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 16:58:18 -0400 Subject: [PATCH 316/589] mai2: add image validation via Pillow --- requirements.txt | 1 + titles/mai2/base.py | 46 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6d37728..53d867a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ bcrypt jinja2 protobuf autobahn +pillow diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 267edec..f1b04b6 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -2,7 +2,8 @@ from datetime import datetime from typing import Any, Dict, List import logging from base64 import b64decode -from os import path, stat +from os import path, stat, remove +from PIL import ImageFile from core.config import CoreConfig from titles.mai2.const import Mai2Constants @@ -91,7 +92,7 @@ class Mai2Base: for i, charge in enumerate(game_charge_list): charge_list.append( { - "orderId": i, + "orderId": i + 1, "chargeId": charge["ticketId"], "price": charge["price"], "startDate": "2017-12-05 07:00:00.0", @@ -812,27 +813,52 @@ class Mai2Base: self.logger.warn(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + ret_code = order_id + 1 photo_chunk = b64decode(div_data) if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len): self.logger.warn(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}" - if not path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") and div_num != 0: + if not path.exists(f"{out_name}.bin") and div_num != 0: self.logger.warn(f"Out of order photo upload (div_num {div_num})") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - if path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") and div_num == 0: + if path.exists(f"{out_name}.bin") and div_num == 0: self.logger.warn(f"Duplicate file upload") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - elif path.exists(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg"): - fstats = stat(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg") + elif path.exists(f"{out_name}.bin"): + fstats = stat(f"{out_name}.bin") if fstats.st_size != 10240 * div_num: self.logger.warn(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - - with open(f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}.jpeg", "ab") as f: - f.write(photo_chunk) + + try: + with open(f"{out_name}.bin", "ab") as f: + f.write(photo_chunk) + + except Exception: + self.logger.error(f"Failed writing to {out_name}.bin") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - return {'returnCode': order_id + 1, 'apiName': 'UploadUserPhotoApi'} + if div_num + 1 == div_len and path.exists(f"{out_name}.bin"): + try: + p = ImageFile.Parser() + with open(f"{out_name}.bin", "rb") as f: + p.feed(f.read()) + + im = p.close() + im.save(f"{out_name}.jpeg") + except Exception: + self.logger.error(f"File {out_name}.bin failed image validation") + + try: + remove(f"{out_name}.bin") + + except Exception: + self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") + + return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} From 14a315a673bc3699082aaa6b5eb0e6ed97e840f9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 16:58:34 -0400 Subject: [PATCH 317/589] replace except with except Exception --- core/aimedb.py | 2 +- core/data/schema/base.py | 4 ++-- core/frontend.py | 2 +- core/mucha.py | 13 ++++++++----- titles/chuni/base.py | 2 +- titles/cxb/base.py | 10 +++++----- titles/cxb/read.py | 2 +- titles/mai2/index.py | 4 ++-- titles/sao/read.py | 14 +++++++------- titles/wacca/base.py | 2 +- titles/wacca/const.py | 2 +- titles/wacca/index.py | 2 +- titles/wacca/lily.py | 2 +- titles/wacca/reverse.py | 2 +- 14 files changed, 33 insertions(+), 30 deletions(-) diff --git a/core/aimedb.py b/core/aimedb.py index 64bac8d..39d6373 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -63,7 +63,7 @@ class AimedbProtocol(Protocol): try: decrypted = cipher.decrypt(data) - except: + except Exception: self.logger.error(f"Failed to decrypt {data.hex()}") return None diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 319101f..a53392f 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -58,7 +58,7 @@ class BaseData: self.logger.error(f"UnicodeEncodeError error {e}") return None - except: + except Exception: try: res = self.conn.execute(sql, opts) @@ -70,7 +70,7 @@ class BaseData: self.logger.error(f"UnicodeEncodeError error {e}") return None - except: + except Exception: self.logger.error(f"Unknown error") raise diff --git a/core/frontend.py b/core/frontend.py index 9eb30e6..f01be50 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -130,7 +130,7 @@ class FE_Gate(FE_Base): if b"e" in request.args: try: err = int(request.args[b"e"][0].decode()) - except: + except Exception: err = 0 else: diff --git a/core/mucha.py b/core/mucha.py index 6bb34d7..8d9dd8e 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -68,10 +68,13 @@ class MuchaServlet: return b"RESULTS=000" # TODO: Decrypt S/N + b_key = b"" + for x in range(8): + b_key += req.sendDate[(x - 1) & 7].encode() - #cipher = Blowfish.new(req.sendDate.encode(), Blowfish.MODE_ECB) - #sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum)) - #self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") + cipher = Blowfish.new(b_key, Blowfish.MODE_ECB) + sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum)) + self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") resp = MuchaAuthResponse( f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" @@ -131,7 +134,7 @@ class MuchaServlet: return ret - except: + except Exception: self.logger.error(f"Error processing mucha request {data}") return None @@ -143,7 +146,7 @@ class MuchaServlet: return urlencode.encode() - except: + except Exception: self.logger.error("Error processing mucha response") return None diff --git a/titles/chuni/base.py b/titles/chuni/base.py index ed8d0fb..4c4361c 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -644,7 +644,7 @@ class ChuniBase: upsert["userData"][0]["userName"] = self.read_wtf8( upsert["userData"][0]["userName"] ) - except: + except Exception: pass self.data.profile.put_profile_data( diff --git a/titles/cxb/base.py b/titles/cxb/base.py index a649f31..89e9cc3 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -197,7 +197,7 @@ class CxbBase: v_profile = self.data.profile.get_profile_index(0, uid, self.version) v_profile_data = v_profile["data"] versionindex.append(int(v_profile_data["appVersion"])) - except: + except Exception: versionindex.append("10400") def handle_action_loadrange_request(self, data: Dict) -> Dict: @@ -286,7 +286,7 @@ class CxbBase: # REV Omnimix Version Fetcher gameversion = data["saveindex"]["data"][0][2] self.logger.warning(f"Game Version is {gameversion}") - except: + except Exception: pass if "10205" in gameversion: @@ -348,7 +348,7 @@ class CxbBase: # Sunrise try: profileIndex = save_data["index"].index("0") - except: + except Exception: return {"data": ""} # Maybe profile = json.loads(save_data["data"][profileIndex]) @@ -496,7 +496,7 @@ class CxbBase: score=int(rid["sc"][0]), clear=rid["clear"], ) - except: + except Exception: self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), @@ -514,7 +514,7 @@ class CxbBase: score=int(rid["sc"][0]), clear=0, ) - except: + except Exception: self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), diff --git a/titles/cxb/read.py b/titles/cxb/read.py index cf2d8e1..06a171f 100644 --- a/titles/cxb/read.py +++ b/titles/cxb/read.py @@ -123,5 +123,5 @@ class CxbReader(BaseReader): genre, int(row["easy"].replace("Easy ", "").replace("N/A", "0")), ) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/mai2/index.py b/titles/mai2/index.py index b54832e..9652fc8 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -113,13 +113,13 @@ class Mai2Servlet: if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir): try: mkdir(self.game_cfg.uploads.photos_dir) - except: + except Exception: self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}") if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir): try: mkdir(self.game_cfg.uploads.movies_dir) - except: + except Exception: self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: diff --git a/titles/sao/read.py b/titles/sao/read.py index d70c275..649fa02 100644 --- a/titles/sao/read.py +++ b/titles/sao/read.py @@ -65,7 +65,7 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading HeroLog.csv") @@ -99,7 +99,7 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading Equipment.csv") @@ -131,7 +131,7 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading Item.csv") @@ -161,7 +161,7 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading SupportLog.csv") @@ -193,7 +193,7 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading Title.csv") @@ -226,7 +226,7 @@ class SaoReader(BaseReader): print(err) elif len(titleId) < 6: # current server code cannot have multiple lengths for the id continue - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.info("Now reading RareDropTable.csv") @@ -250,5 +250,5 @@ class SaoReader(BaseReader): ) except Exception as err: print(err) - except: + except Exception: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/wacca/base.py b/titles/wacca/base.py index cca6aa5..2351001 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -426,7 +426,7 @@ class WaccaBase: elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: + except Exception: self.logger.error( f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" ) diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 284d236..b25d3ac 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -221,5 +221,5 @@ class WaccaConstants: cls.Region.YAMANASHI, cls.Region.WAKAYAMA, ][region] - except: + except Exception: return None diff --git a/titles/wacca/index.py b/titles/wacca/index.py index a59cda1..e36c295 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -93,7 +93,7 @@ class WaccaServlet: try: req_json = json.loads(request.content.getvalue()) version_full = Version(req_json["appVersion"]) - except: + except Exception: self.logger.error( f"Failed to parse request to {url_path} -> {request.content.getvalue()}" ) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 6ac60de..3ac03fa 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -424,7 +424,7 @@ class WaccaLily(WaccaS): elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: + except Exception: self.logger.error( f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" ) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 1711013..728ef0a 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -289,7 +289,7 @@ class WaccaReverse(WaccaLilyR): elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: + except Exception: self.logger.error( f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" ) From 7c78975431bd0115c8508b5560a850ad6ddfe98a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 17:00:52 -0400 Subject: [PATCH 318/589] mai2: update example config --- example_config/mai2.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index d89f5d7..8557151 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -5,4 +5,10 @@ server: deliver: enable: False udbdl_enable: False - content_folder: "" \ No newline at end of file + content_folder: "" + +uploads: + photos: False + photos_dir: "" + movies: False + movies_dir: "" From 718229b267825773087bb5b0153b35cd0351379c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 17:21:45 -0400 Subject: [PATCH 319/589] Update changelog --- changelog.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/changelog.md b/changelog.md index e1b5642..31c11a3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,52 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20230716 +### General ++ Docker files added (#19) ++ Added support for threading + + This comes with the caviat that enabling it will not allow you to use Ctrl + C to stop the server. + +### Webui ++ Small improvements ++ Add card display + +### Allnet ++ Billing format validation ++ Fix naomitest.html endpoint ++ Add event logging for auths and billing ++ LoaderStateRecorder endpoint handler added + +### Mucha ++ Fixed log level always being "Info" ++ Add stub handler for DownloadState + +### Sward Art Online ++ Support added + +### Card Maker ++ DX Passes fixed ++ Various improvements + +### Diva ++ Added clear status calculation ++ Various minor fixes and improvements + +### Maimai ++ Added support for memorial photo uploads ++ Added support for the following versions + + Festival + + FiNALE ++ Various bug fixes and improvements + +### Wacca ++ Fixed an error that sometimes occoured when trying to unlock songs (#22) + +### Pokken ++ Profile saving added (loading TBA) ++ Use external STUN server for matching by default + + Matching still not working + ## 2023042300 ### Wacca + Time free now works properly From 63d81a270450bd3b8454cee503b1c0e773ac45b0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 16 Jul 2023 17:24:07 -0400 Subject: [PATCH 320/589] changelog: fix typos --- changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 31c11a3..40d2e48 100644 --- a/changelog.md +++ b/changelog.md @@ -21,9 +21,13 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu + Fixed log level always being "Info" + Add stub handler for DownloadState -### Sward Art Online +### Sword Art Online + Support added +### Crossbeats ++ Added threading to profile loading + + This should cause a noticeable speed-up + ### Card Maker + DX Passes fixed + Various improvements From 20335aaebe26efc5b3a12f9bdce7ddfce5299269 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 12:47:10 -0400 Subject: [PATCH 321/589] add download report api --- core/allnet.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++-- index.py | 2 +- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 9ad5949..7946fdd 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -6,6 +6,8 @@ from datetime import datetime import pytz import base64 import zlib +import json +from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 @@ -18,6 +20,9 @@ from core.utils import Utils from core.data import Data from core.const import * +class DLIMG_TYPE(Enum): + app = 0 + opt = 1 class AllnetServlet: def __init__(self, core_cfg: CoreConfig, cfg_folder: str): @@ -241,6 +246,7 @@ class AllnetServlet: if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") + return open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" ).read() @@ -249,10 +255,31 @@ class AllnetServlet: return b"" def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: - self.logger.info( - f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}" - ) - return b"" + req_raw = request.content.getvalue() + try: + req_dict: Dict = json.loads(req_raw) + except Exception as e: + self.logger.warn(f"Failed to parse DL Report: {e}") + return "NG" + + dl_data_type = DLIMG_TYPE.app + dl_data = req_dict.get("appimage", {}) + + if dl_data is None or not dl_data: + dl_data_type = DLIMG_TYPE.opt + dl_data = req_dict.get("optimage", {}) + + if dl_data is None or not dl_data: + self.logger.warn(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") + return "NG" + + dl_report_data = DLReport(dl_data, dl_data_type) + + if not dl_report_data.validate(): + self.logger.warn(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}") + return "NG" + + return "OK" def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: req_data = request.content.getvalue() @@ -529,3 +556,86 @@ class AllnetRequestException(Exception): def __init__(self, message="") -> None: self.message = message super().__init__(self.message) + +class DLReport: + def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None: + self.serial = data.get("serial") + self.dfl = data.get("dfl") + self.wfl = data.get("wfl") + self.tsc = data.get("tsc") + self.tdsc = data.get("tdsc") + self.at = data.get("at") + self.ot = data.get("ot") + self.rt = data.get("rt") + self.as_ = data.get("as") + self.rf_state = data.get("rf_state") + self.gd = data.get("gd") + self.dav = data.get("dav") + self.wdav = data.get("wdav") # app only + self.dov = data.get("dov") + self.wdov = data.get("wdov") # app only + self.__type = report_type + self.err = "" + + def validate(self) -> bool: + if self.serial is None: + self.err = "serial not provided" + return False + + if self.dfl is None: + self.err = "dfl not provided" + return False + + if self.wfl is None: + self.err = "wfl not provided" + return False + + if self.tsc is None: + self.err = "tsc not provided" + return False + + if self.tdsc is None: + self.err = "tdsc not provided" + return False + + if self.at is None: + self.err = "at not provided" + return False + + if self.ot is None: + self.err = "ot not provided" + return False + + if self.rt is None: + self.err = "rt not provided" + return False + + if self.as_ is None: + self.err = "as not provided" + return False + + if self.rf_state is None: + self.err = "rf_state not provided" + return False + + if self.gd is None: + self.err = "gd not provided" + return False + + if self.dav is None: + self.err = "dav not provided" + return False + + if self.dov is None: + self.err = "dov not provided" + return False + + if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app: + self.err = "wdav or wdov not provided in app image" + return False + + if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt: + self.err = "wdav or wdov provided in opt image" + return False + + return True diff --git a/index.py b/index.py index 2ff7c04..cb569a0 100644 --- a/index.py +++ b/index.py @@ -36,7 +36,7 @@ class HttpDispatcher(resource.Resource): self.map_post.connect( "allnet_downloadorder_report", - "/dl/report", + "/report-api/Report", controller="allnet", action="handle_dlorder_report", conditions=dict(method=["POST"]), From f417be671b9ead975148e6ca9f28305880f300cb Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 12:47:37 -0400 Subject: [PATCH 322/589] pokken: fix typo --- titles/pokken/const.py | 1 + titles/pokken/schema/item.py | 6 +++++- titles/pokken/schema/profile.py | 10 +++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index e7ffdd8..9fa3b06 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -15,6 +15,7 @@ class PokkenConstants: AI = 2 LAN = 3 WAN = 4 + TUTORIAL_3 = 7 class BATTLE_RESULT(Enum): WIN = 1 diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py index 32bff2a..6c13306 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -39,7 +39,11 @@ class PokkenItemData(BaseData): type=item_type, ) - result = self.execute(sql) + conflict = sql.on_duplicate_key_update( + content=content, + ) + + result = self.execute(conflict) if result is None: self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}") return None diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 812964d..1d745f6 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -259,7 +259,7 @@ class PokkenProfileData(BaseData): illustration_book_no=illust_no, bp_point_atk=atk, bp_point_res=res, - bp_point_defe=defe, + bp_point_def=defe, bp_point_sp=sp, ) @@ -267,7 +267,7 @@ class PokkenProfileData(BaseData): illustration_book_no=illust_no, bp_point_atk=atk, bp_point_res=res, - bp_point_defe=defe, + bp_point_def=defe, bp_point_sp=sp, ) @@ -347,7 +347,11 @@ class PokkenProfileData(BaseData): if result is None: self.logger.warn(f"Failed to update stats for user {user_id}") - def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None: + def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None: + if support1 == 4294967295: + support1 = None + if support2 == 4294967295: + support2 = None sql = update(profile).where(profile.c.user==user_id).values( support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1, support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2, From b94380790463055bdfe97f11e4681a6f493c1342 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 22:21:41 -0400 Subject: [PATCH 323/589] core: add columns to machine table, bump to v5 --- core/data/database.py | 2 +- core/data/schema/arcade.py | 44 ++++++++++++++++--- core/data/schema/user.py | 14 ++++++ core/data/schema/versions/CORE_4_rollback.sql | 3 ++ core/data/schema/versions/CORE_5_upgrade.sql | 3 ++ 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 core/data/schema/versions/CORE_4_rollback.sql create mode 100644 core/data/schema/versions/CORE_5_upgrade.sql diff --git a/core/data/database.py b/core/data/database.py index 9fb2606..51c170b 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -15,7 +15,7 @@ from core.utils import Utils class Data: - current_schema_version = 4 + current_schema_version = 5 engine = None session = None user = None diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index e1d9b1f..c45541b 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -1,9 +1,10 @@ -from typing import Optional, Dict -from sqlalchemy import Table, Column +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, and_, or_ from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint -from sqlalchemy.types import Integer, String, Boolean +from sqlalchemy.types import Integer, String, Boolean, JSON from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row import re from core.data.schema.base import BaseData, metadata @@ -40,6 +41,9 @@ machine = Table( Column("timezone", String(255)), Column("ota_enable", Boolean), Column("is_cab", Boolean), + Column("memo", String(255)), + Column("is_cab", Boolean), + Column("data", JSON), mysql_charset="utf8mb4", ) @@ -65,7 +69,7 @@ arcade_owner = Table( class ArcadeData(BaseData): - def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: + def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: if serial is not None: serial = serial.replace("-", "") if len(serial) == 11: @@ -130,12 +134,19 @@ class ArcadeData(BaseData): f"Failed to update board id for machine {machine_id} -> {boardid}" ) - def get_arcade(self, id: int) -> Optional[Dict]: + def get_arcade(self, id: int) -> Optional[Row]: sql = arcade.select(arcade.c.id == id) result = self.execute(sql) if result is None: return None return result.fetchone() + + def get_arcade_machines(self, id: int) -> Optional[List[Row]]: + sql = machine.select(machine.c.arcade == id) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def put_arcade( self, @@ -165,7 +176,21 @@ class ArcadeData(BaseData): return None return result.lastrowid - def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: + def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]: + sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() + + def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]: + sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id)) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) result = self.execute(sql) @@ -217,3 +242,10 @@ class ArcadeData(BaseData): return False return True + + def find_arcade_by_name(self, name: str) -> List[Row]: + sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 6a95005..221ba81 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -107,3 +107,17 @@ class UserData(BaseData): if result is None: return None return result.fetchall() + + def find_user_by_email(self, email: str) -> Row: + sql = select(aime_user).where(aime_user.c.email == email) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def find_user_by_username(self, username: str) -> List[Row]: + sql = aime_user.select(aime_user.c.username.like(f"%{username}%")) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() diff --git a/core/data/schema/versions/CORE_4_rollback.sql b/core/data/schema/versions/CORE_4_rollback.sql new file mode 100644 index 0000000..4464915 --- /dev/null +++ b/core/data/schema/versions/CORE_4_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine DROP COLUMN memo; +ALTER TABLE machine DROP COLUMN is_blacklisted; +ALTER TABLE machine DROP COLUMN `data`; diff --git a/core/data/schema/versions/CORE_5_upgrade.sql b/core/data/schema/versions/CORE_5_upgrade.sql new file mode 100644 index 0000000..8e88b00 --- /dev/null +++ b/core/data/schema/versions/CORE_5_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine ADD memo varchar(255) NULL; +ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL; +ALTER TABLE machine ADD `data` longtext NULL; From 6c89a97fe34f28920996c3d13414d28db36dfcb5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 22:21:49 -0400 Subject: [PATCH 324/589] frontend: add management pages --- core/frontend.py | 200 ++++++++++++++++++++++++- core/frontend/arcade/index.jinja | 4 + core/frontend/machine/index.jinja | 5 + core/frontend/sys/index.jinja | 98 ++++++++++++ core/frontend/user/index.jinja | 14 +- core/frontend/widgets/err_banner.jinja | 4 + core/frontend/widgets/topbar.jinja | 3 + 7 files changed, 321 insertions(+), 7 deletions(-) create mode 100644 core/frontend/arcade/index.jinja create mode 100644 core/frontend/machine/index.jinja create mode 100644 core/frontend/sys/index.jinja diff --git a/core/frontend.py b/core/frontend.py index f01be50..a79bd94 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -9,6 +9,9 @@ from zope.interface import Interface, Attribute, implementer from twisted.python.components import registerAdapter import jinja2 import bcrypt +import re +from enum import Enum +from urllib import parse from core import CoreConfig, Utils from core.data import Data @@ -19,6 +22,13 @@ class IUserSession(Interface): current_ip = Attribute("User's current ip address") permissions = Attribute("User's permission level") +class PermissionOffset(Enum): + USER = 0 # Regular user + USERMOD = 1 # Can moderate other users + ACMOD = 2 # Can add arcades and cabs + SYSADMIN = 3 # Can change settings + # 4 - 6 reserved for future use + OWNER = 7 # Can do anything @implementer(IUserSession) class UserSession(object): @@ -80,6 +90,9 @@ class FrontendServlet(resource.Resource): self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) self.putChild(b"user", FE_User(cfg, self.environment)) + self.putChild(b"sys", FE_System(cfg, self.environment)) + self.putChild(b"arcade", FE_Arcade(cfg, self.environment)) + self.putChild(b"cab", FE_Machine(cfg, self.environment)) self.putChild(b"game", fe_game) self.logger.info( @@ -154,6 +167,7 @@ class FE_Gate(FE_Base): passwd = None uid = self.data.card.get_user_id_from_card(access_code) + user = self.data.user.get_user(uid) if uid is None: return redirectTo(b"/gate?e=1", request) @@ -175,6 +189,7 @@ class FE_Gate(FE_Base): usr_sesh = IUserSession(sesh) usr_sesh.userId = uid usr_sesh.current_ip = ip + usr_sesh.permissions = user['permissions'] return redirectTo(b"/user", request) @@ -192,7 +207,7 @@ class FE_Gate(FE_Base): hashed = bcrypt.hashpw(passwd, salt) result = self.data.user.create_user( - uid, username, email, hashed.decode(), 1 + uid, username, email.lower(), hashed.decode(), 1 ) if result is None: return redirectTo(b"/gate?e=3", request) @@ -210,17 +225,29 @@ class FE_Gate(FE_Base): return redirectTo(b"/gate?e=2", request) ac = request.args[b"ac"][0].decode() + card = self.data.card.get_card_by_access_code(ac) + if card is None: + return redirectTo(b"/gate?e=1", request) + + user = self.data.user.get_user(card['user']) + if user is None: + self.logger.warn(f"Card {ac} exists with no/invalid associated user ID {card['user']}") + return redirectTo(b"/gate?e=0", request) + + if user['password'] is not None: + return redirectTo(b"/gate?e=1", request) template = self.environment.get_template("core/frontend/gate/create.jinja") return template.render( title=f"{self.core_config.server.name} | Create User", code=ac, - sesh={"userId": 0}, + sesh={"userId": 0, "permissions": 0}, ).encode("utf-16") class FE_User(FE_Base): def render_GET(self, request: Request): + uri = request.uri.decode() template = self.environment.get_template("core/frontend/user/index.jinja") sesh: Session = request.getSession() @@ -228,9 +255,26 @@ class FE_User(FE_Base): if usr_sesh.userId == 0: return redirectTo(b"/gate", request) - cards = self.data.card.get_user_cards(usr_sesh.userId) - user = self.data.user.get_user(usr_sesh.userId) + m = re.match("\/user\/(\d*)", uri) + + if m is not None: + usrid = m.group(1) + if usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value or not usrid == usr_sesh.userId: + return redirectTo(b"/user", request) + + else: + usrid = usr_sesh.userId + + user = self.data.user.get_user(usrid) + if user is None: + return redirectTo(b"/user", request) + + cards = self.data.card.get_user_cards(usrid) + arcades = self.data.arcade.get_arcades_managed_by_user(usrid) + card_data = [] + arcade_data = [] + for c in cards: if c['is_locked']: status = 'Locked' @@ -240,9 +284,104 @@ class FE_User(FE_Base): status = 'Active' card_data.append({'access_code': c['access_code'], 'status': status}) + + for a in arcades: + arcade_data.append({'id': a['id'], 'name': a['name']}) return template.render( - title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username'] + title=f"{self.core_config.server.name} | Account", + sesh=vars(usr_sesh), + cards=card_data, + username=user['username'], + arcades=arcade_data + ).encode("utf-16") + + def render_POST(self, request: Request): + pass + + +class FE_System(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/sys/index.jinja") + usrlist = [] + aclist = [] + cablist = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0 or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value: + return redirectTo(b"/gate", request) + + if uri.startswith("/sys/lookup.user?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.user?", "")) # lop off the first bit + uid_search = uri_parse.get("usrId") + email_search = uri_parse.get("usrEmail") + uname_search = uri_parse.get("usrName") + + if uid_search is not None: + u = self.data.user.get_user(uid_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif email_search is not None: + u = self.data.user.find_user_by_email(email_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif uname_search is not None: + ul = self.data.user.find_user_by_username(uname_search[0]) + for u in ul: + usrlist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.arcade?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.arcade?", "")) # lop off the first bit + ac_id_search = uri_parse.get("arcadeId") + ac_name_search = uri_parse.get("arcadeName") + ac_user_search = uri_parse.get("arcadeUser") + + if ac_id_search is not None: + u = self.data.arcade.get_arcade(ac_id_search[0]) + if u is not None: + aclist.append(u._asdict()) + + elif ac_name_search is not None: + ul = self.data.arcade.find_arcade_by_name(ac_name_search[0]) + for u in ul: + aclist.append(u._asdict()) + + elif ac_user_search is not None: + ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) + for u in ul: + aclist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.cab?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.cab?", "")) # lop off the first bit + cab_id_search = uri_parse.get("cabId") + cab_serial_search = uri_parse.get("cabSerial") + cab_acid_search = uri_parse.get("cabAcId") + + if cab_id_search is not None: + u = self.data.arcade.get_machine(id=cab_id_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_serial_search is not None: + u = self.data.arcade.get_machine(serial=cab_serial_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_acid_search is not None: + ul = self.data.arcade.get_arcade_machines(cab_acid_search[0]) + for u in ul: + cablist.append(u._asdict()) + + return template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=usrlist, + aclist=aclist, + cablist=cablist, ).encode("utf-16") @@ -257,3 +396,54 @@ class FE_Game(FE_Base): def render_GET(self, request: Request) -> bytes: return redirectTo(b"/user", request) + + +class FE_Arcade(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/arcade/index.jinja") + managed = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + m = re.match("\/arcade\/(\d*)", uri) + + if m is not None: + arcadeid = m.group(1) + perms = self.data.arcade.get_manager_permissions(usr_sesh.userId, arcadeid) + arcade = self.data.arcade.get_arcade(arcadeid) + + if perms is None: + perms = 0 + + else: + return redirectTo(b"/user", request) + + return template.render( + title=f"{self.core_config.server.name} | Arcade", + sesh=vars(usr_sesh), + error=0, + perms=perms, + arcade=arcade._asdict() + ).encode("utf-16") + + +class FE_Machine(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/machine/index.jinja") + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + return template.render( + title=f"{self.core_config.server.name} | Machine", + sesh=vars(usr_sesh), + arcade={}, + error=0, + ).encode("utf-16") \ No newline at end of file diff --git a/core/frontend/arcade/index.jinja b/core/frontend/arcade/index.jinja new file mode 100644 index 0000000..20a1f46 --- /dev/null +++ b/core/frontend/arcade/index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

{{ arcade.name }}

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

Machine Management

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

System Management

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

User Search

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

Arcade Search

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

Machine Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} +
+
+ {% if sesh.permissions >= 2 %} +
+ {% for usr in usrlist %} +
{{ usr.id }} | {{ usr.username }}
+ {% endfor %} +
+ {% endif %} + {% if sesh.permissions >= 4 %} +
+ {% for ac in aclist %} +
{{ ac.id }} | {{ ac.name }}
+ {% endfor %} +
+ {% endif %} +
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja index 2911e67..2f76b14 100644 --- a/core/frontend/user/index.jinja +++ b/core/frontend/user/index.jinja @@ -2,11 +2,21 @@ {% block content %}

Management for {{ username }}

Cards

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

    Arcades

    + +{% endif %} +