From c99bfda01545d9dabc5425e67a7abf5ca5877459 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sat, 18 Feb 2023 21:01:31 +0100 Subject: [PATCH 01/39] 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 8bdc2071dab2941fb5feb821832758214377378e Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 19 Feb 2023 16:08:53 +0100 Subject: [PATCH 02/39] 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 03/39] 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 04/39] 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 05/39] 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 06/39] 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 3f40e083ce7687f0592d85bee4c632e52fd475ff Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 21 Feb 2023 16:46:43 -0500 Subject: [PATCH 07/39] 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 08/39] 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 fff7eb46660a1b880f8014ccc97dae0bf509e907 Mon Sep 17 00:00:00 2001 From: God601 Date: Wed, 22 Feb 2023 11:47:32 -0500 Subject: [PATCH 09/39] 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 10/39] 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 11/39] 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 12/39] 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 13/39] 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 14/39] 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 15/39] 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 16/39] 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 2bd980165eff134cf1440ec843f48fb381027471 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 24 Feb 2023 13:38:31 -0500 Subject: [PATCH 17/39] 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 18/39] 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 19/39] 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 20/39] 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 21/39] 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 22/39] 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 23/39] 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 24/39] 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 25/39] 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 26/39] 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 27/39] 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 28/39] 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 29/39] 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 30/39] 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 31/39] 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 32/39] 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 33/39] 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 34/39] 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 35/39] 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 36/39] 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 37/39] 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 38/39] 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 39/39] 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()