From f63dd079370aca81378fd221cd545ae7fc67846b Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 10 Apr 2023 18:58:19 +0200 Subject: [PATCH 01/36] maimai: Initial Festival support --- core/data/schema/versions/SDEZ_3_rollback.sql | 31 ++ core/data/schema/versions/SDEZ_4_upgrade.sql | 31 ++ docs/game_specific_info.md | 3 +- readme.md | 4 +- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 278 ++++++++++-------- titles/mai2/const.py | 14 +- titles/mai2/festival.py | 31 ++ titles/mai2/index.py | 45 +-- titles/mai2/schema/item.py | 72 +++-- titles/mai2/schema/profile.py | 22 +- titles/mai2/schema/score.py | 2 + titles/mai2/schema/static.py | 5 +- titles/mai2/universeplus.py | 5 +- 14 files changed, 347 insertions(+), 198 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_3_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_4_upgrade.sql create mode 100644 titles/mai2/festival.py diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql new file mode 100644 index 0000000..f819855 --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +DROP COLUMN tapSe; + +ALTER TABLE mai2_score_best +DROP COLUMN extNum1; + +ALTER TABLE mai2_profile_extend +DROP COLUMN playStatusSetting; + +ALTER TABLE mai2_playlog +DROP COLUMN extNum4; + +ALTER TABLE mai2_static_event +DROP COLUMN startDate; + +ALTER TABLE mai2_item_map +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN isLock is_lock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, +CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, +CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonusId bonus_id INT NOT NULL, +CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql new file mode 100644 index 0000000..52d120a --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +ADD COLUMN tapSe INT NOT NULL DEFAULT 0 AFTER tapDesign; + +ALTER TABLE mai2_score_best +ADD COLUMN extNum1 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_profile_extend +ADD COLUMN playStatusSetting INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_playlog +ADD COLUMN extNum4 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_static_event +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE mai2_item_map +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN is_lock isLock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_clear isClear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN season_id seasonId INT NOT NULL, +CHANGE COLUMN reward_get rewardGet BOOLEAN NOT NULL, +CHANGE COLUMN user_name userName VARCHAR(8) NOT NULL, +CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonus_id bonusId INT NOT NULL, +CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index b09d61e..1f7872c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -114,6 +114,7 @@ Config file is located in `config/cxb.yaml`. | 3 | maimai DX Splash PLUS | | 4 | maimai DX Universe | | 5 | maimai DX Universe PLUS | +| 6 | maimai DX Festival | ### Importer @@ -126,7 +127,7 @@ python read.py --series SDEZ --version --binfolder /path/to/game/fo The importer for maimai DX will import Events, Music and Tickets. **NOTE: It is required to use the importer because the game will -crash without it!** +crash without Events!** ### Database upgrade diff --git a/readme.md b/readme.md index 4afc225..02f0c67 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + Crossbeats Rev + All versions + omnimix -+ Maimai - + All versions up to Universe Plus ++ maimai DX + + All versions up to Festival + Hatsune Miku Arcade + All versions diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 27fba3a..810eac9 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -7,4 +7,4 @@ index = Mai2Servlet database = Mai2Data reader = Mai2Reader game_codes = [Mai2Constants.GAME_CODE] -current_schema_version = 3 +current_schema_version = 4 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 741ccb6..2e7198d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,5 +1,5 @@ from datetime import datetime, date, timedelta -from typing import Dict +from typing import Any, Dict import logging from core.config import CoreConfig @@ -52,6 +52,7 @@ class Mai2Base: events = self.data.static.get_enabled_events(self.version) events_lst = [] if events is None: + self.logger.warn("No enabled events, did you run the reader?") return {"type": data["type"], "length": 0, "gameEventList": []} for event in events: @@ -59,7 +60,11 @@ class Mai2Base: { "type": event["type"], "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + event["startDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), "endDate": "2099-12-31 00:00:00.0", } ) @@ -79,12 +84,12 @@ class Mai2Base: return {"length": 0, "gameChargeList": []} charge_list = [] - for x in range(len(game_charge_list)): + for i, charge in enumerate(game_charge_list): charge_list.append( { - "orderId": x, - "chargeId": game_charge_list[x]["ticketId"], - "price": game_charge_list[x]["price"], + "orderId": i, + "chargeId": charge["ticketId"], + "price": charge["price"], "startDate": "2017-12-05 07:00:00.0", "endDate": "2099-12-31 00:00:00.0", } @@ -167,6 +172,20 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -204,15 +223,21 @@ class Mai2Base: if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], - datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"), - datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S") + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), ) - if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: self.data.item.put_character( user_id, @@ -222,7 +247,7 @@ class Mai2Base: char["useCount"], ) - if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0: + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: self.data.item.put_item( user_id, @@ -232,7 +257,7 @@ class Mai2Base: item["isValid"], ) - if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0: + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: self.data.item.put_login_bonus( user_id, @@ -242,7 +267,7 @@ class Mai2Base: login_bonus["isComplete"], ) - if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0: + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: self.data.item.put_map( user_id, @@ -253,21 +278,27 @@ class Mai2Base: map["isComplete"], ) - if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0: + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, music) - if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0: + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: self.data.score.put_course(user_id, course) - if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0: + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - # if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: - # for fsr in upsert["userFriendSeasonRankingList"]: - # pass + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime(fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) def handle_user_logout_api_request(self, data: Dict) -> Dict: pass @@ -311,11 +342,7 @@ class Mai2Base: def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: - return { - "userId": data["userId"], - "nextIndex": 0, - "userCardList": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -333,25 +360,23 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["startDate"] = datetime.strftime( - tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["endDate"] = datetime.strftime( - tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) card_list.append(tmp) return { "userId": data["userId"], "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx] + "userCardList": card_list[start_idx:end_idx], } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charges = self.data.item.get_charges(data["userId"]) if user_charges is None: - return { - "userId": data["userId"], - "length": 0, - "userChargeList": [] - } + return {"userId": data["userId"], "length": 0, "userChargeList": []} user_charge_list = [] for charge in user_charges: @@ -359,45 +384,46 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["purchaseDate"] = datetime.strftime( - tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S") + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["validDate"] = datetime.strftime( - tmp["validDate"], "%Y-%m-%d %H:%M:%S") + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) user_charge_list.append(tmp) return { "userId": data["userId"], "length": len(user_charge_list), - "userChargeList": user_charge_list + "userChargeList": user_charge_list, } def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_items = self.data.item.get_items(data["userId"], kind) - user_item_list = [] - next_idx = 0 + user_item_list = self.data.item.get_items(data["userId"], kind) - for x in range(next_idx, data["maxCount"]): - try: - user_item_list.append({ - "itemKind": user_items[x]["itemKind"], - "itemId": user_items[x]["itemId"], - "stock": user_items[x]["stock"], - "isValid": user_items[x]["isValid"] - }) - except IndexError: + items: list[Dict[str, Any]] = [] + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): break - if len(user_item_list) == data["maxCount"]: - next_idx = data["nextIndex"] + data["maxCount"] + 1 - break + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout return { "userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, - "userItemList": user_item_list + "userItemList": items, } def handle_get_user_character_api_request(self, data: Dict) -> Dict: @@ -479,21 +505,12 @@ class Mai2Base: tmp.pop("user") mlst.append(tmp) - return { - "userActivity": { - "playList": plst, - "musicList": mlst - } - } + return {"userActivity": {"playList": plst, "musicList": mlst}} def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) if user_courses is None: - return { - "userId": data["userId"], - "nextIndex": 0, - "userCourseList": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} course_list = [] for course in user_courses: @@ -502,11 +519,7 @@ class Mai2Base: tmp.pop("id") course_list.append(tmp) - return { - "userId": data["userId"], - "nextIndex": 0, - "userCourseList": course_list - } + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps @@ -514,96 +527,103 @@ class Mai2Base: def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) - friend_season_ranking_list = [] - next_index = 0 + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - friend_season_ranking_list.append( - { - "mapId": friend_season_ranking_list[x]["map_id"], - "distance": friend_season_ranking_list[x]["distance"], - "isLock": friend_season_ranking_list[x]["is_lock"], - "isClear": friend_season_ranking_list[x]["is_clear"], - "isComplete": friend_season_ranking_list[x]["is_complete"], - } - ) - except: + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(friend_season_ranking_list) == data["maxCount"] - and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userFriendSeasonRankingList": friend_season_ranking_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) - map_list = [] - next_index = 0 + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - map_list.append( - { - "mapId": maps[x]["map_id"], - "distance": maps[x]["distance"], - "isLock": maps[x]["is_lock"], - "isClear": maps[x]["is_clear"], - "isComplete": maps[x]["is_complete"], - } - ) - except: + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(map_list) == data["maxCount"] - and len(maps) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userMapList": map_list, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) - login_bonus_list = [] - next_index = 0 + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - login_bonus_list.append( - { - "bonusId": login_bonuses[x]["bonus_id"], - "point": login_bonuses[x]["point"], - "isCurrent": login_bonuses[x]["is_current"], - "isComplete": login_bonuses[x]["is_complete"], - } - ) - except: + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(login_bonus_list) == data["maxCount"] - and len(login_bonuses) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userLoginBonusList": login_bonus_list, } @@ -629,5 +649,5 @@ class Mai2Base: return { "userId": data["userId"], "nextIndex": next_index, - "userMusicList": [{"userMusicDetailList": music_detail_list}] + "userMusicList": [{"userMusicDetailList": music_detail_list}], } diff --git a/titles/mai2/const.py b/titles/mai2/const.py index dd1dca0..9694aba 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -30,14 +30,16 @@ class Mai2Constants: VER_MAIMAI_DX_SPLASH_PLUS = 3 VER_MAIMAI_DX_UNIVERSE = 4 VER_MAIMAI_DX_UNIVERSE_PLUS = 5 + VER_MAIMAI_DX_FESTIVAL = 6 VERSION_STRING = ( - "maimai Delux", - "maimai Delux PLUS", - "maimai Delux Splash", - "maimai Delux Splash PLUS", - "maimai Delux Universe", - "maimai Delux Universe PLUS", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX Universe", + "maimai DX Universe PLUS", + "maimai DX Festival" ) @classmethod diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py new file mode 100644 index 0000000..4e51619 --- /dev/null +++ b/titles/mai2/festival.py @@ -0,0 +1,31 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Festival(Mai2UniversePlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.36 + user_data["lastDataVersion"] = "1.30.00" + return user_data + + def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = super().handle_user_login_api_request(data) + # useless? + user_login["Bearer"] = "ARTEMiSTOKEN" + return user_login + + def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 3cd1629..3618f4d 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -10,6 +10,7 @@ from os import path from typing import Tuple from core.config import CoreConfig +from core.utils import Utils from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants from titles.mai2.base import Mai2Base @@ -18,6 +19,7 @@ from titles.mai2.splash import Mai2Splash from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.universe import Mai2Universe from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.festival import Mai2Festival class Mai2Servlet: @@ -30,12 +32,13 @@ class Mai2Servlet: ) self.versions = [ - Mai2Base(core_cfg, self.game_cfg), - Mai2Plus(core_cfg, self.game_cfg), - Mai2Splash(core_cfg, self.game_cfg), - Mai2SplashPlus(core_cfg, self.game_cfg), - Mai2Universe(core_cfg, self.game_cfg), - Mai2UniversePlus(core_cfg, self.game_cfg), + Mai2Base, + Mai2Plus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival ] self.logger = logging.getLogger("mai2") @@ -97,6 +100,7 @@ class Mai2Servlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX @@ -108,8 +112,10 @@ class Mai2Servlet: internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS elif version >= 120 and version < 125: # Universe internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125: # Universe Plus + elif version >= 125 and version < 130: # Universe Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130: # Festival + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're @@ -128,25 +134,30 @@ class Mai2Servlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) - if not hasattr(self.versions[internal_ver], func_to_find): + if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + resp = {"returnCode": 1} - try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + else: + try: + handler = getattr(handler_cls, func_to_find) + resp = handler(req_data) - except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {"returnCode": 1} - self.logger.info(f"Response {resp}") + self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d64d954..c6b2e7a 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -71,12 +71,12 @@ map = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("map_id", Integer, nullable=False), + Column("mapId", Integer, nullable=False), Column("distance", Integer, nullable=False), - Column("is_lock", Boolean, nullable=False, server_default="0"), - Column("is_clear", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "map_id", name="mai2_item_map_uk"), + Column("isLock", Boolean, nullable=False, server_default="0"), + Column("isClear", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), mysql_charset="utf8mb4", ) @@ -89,11 +89,11 @@ login_bonus = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("bonus_id", Integer, nullable=False), + Column("bonusId", Integer, nullable=False), Column("point", Integer, nullable=False), - Column("is_current", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"), + Column("isCurrent", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), mysql_charset="utf8mb4", ) @@ -106,13 +106,15 @@ friend_season_ranking = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("season_id", Integer, nullable=False), + Column("seasonId", Integer, nullable=False), Column("point", Integer, nullable=False), Column("rank", Integer, nullable=False), - Column("reward_get", Boolean, nullable=False), - Column("user_name", String(8), nullable=False), - Column("record_date", String(255), nullable=False), - UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"), + Column("rewardGet", Boolean, nullable=False), + Column("userName", String(8), nullable=False), + Column("recordDate", TIMESTAMP, nullable=False), + UniqueConstraint( + "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" + ), mysql_charset="utf8mb4", ) @@ -293,18 +295,18 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(map).values( user=user_id, - map_id=map_id, + mapId=map_id, distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) result = self.execute(conflict) @@ -324,7 +326,7 @@ class Mai2ItemData(BaseData): return result.fetchall() def get_map(self, user_id: int, map_id: int) -> Optional[Row]: - sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id)) + sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) result = self.execute(sql) if result is None: @@ -341,16 +343,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(character).values( user=user_id, - character_id=character_id, + characterId=character_id, level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) conflict = sql.on_duplicate_key_update( level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) result = self.execute(conflict) @@ -385,7 +387,25 @@ class Mai2ItemData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() + + def put_friend_season_ranking( + self, aime_id: int, friend_season_ranking_data: Dict + ) -> Optional[int]: + sql = insert(friend_season_ranking).values( + user=aime_id, **friend_season_ranking_data + ) + + conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_friend_season_ranking: failed to insert", + f"friend_season_ranking! aime_id: {aime_id}" + ) + return None + return result.lastrowid def put_favorite( self, user_id: int, kind: int, item_id_list: List[int] diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 1ce8046..b3802e5 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -158,6 +158,7 @@ extend = Table( Column("sortMusicSetting", Integer), Column("selectedCardList", JSON), Column("encountMapNpcList", JSON), + Column("playStatusSetting", Integer, server_default="0"), UniqueConstraint("user", "version", name="mai2_profile_extend_uk"), mysql_charset="utf8mb4", ) @@ -178,6 +179,7 @@ option = Table( Column("slideSpeed", Integer), Column("touchSpeed", Integer), Column("tapDesign", Integer), + Column("tapSe", Integer, server_default="0"), Column("holdDesign", Integer), Column("slideDesign", Integer), Column("starType", Integer), @@ -298,8 +300,8 @@ class Mai2ProfileData(BaseData): def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: sql = select(detail).where( - and_(detail.c.user == user_id, detail.c.version == version) - ) + and_(detail.c.user == user_id, detail.c.version <= version) + ).order_by(detail.c.version.desc()) result = self.execute(sql) if result is None: @@ -323,8 +325,8 @@ class Mai2ProfileData(BaseData): def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: sql = select(ghost).where( - and_(ghost.c.user == user_id, ghost.c.version_int == version) - ) + and_(ghost.c.user == user_id, ghost.c.version_int <= version) + ).order_by(ghost.c.version.desc()) result = self.execute(sql) if result is None: @@ -348,8 +350,8 @@ class Mai2ProfileData(BaseData): def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: sql = select(extend).where( - and_(extend.c.user == user_id, extend.c.version == version) - ) + and_(extend.c.user == user_id, extend.c.version <= version) + ).order_by(extend.c.version.desc()) result = self.execute(sql) if result is None: @@ -373,8 +375,8 @@ class Mai2ProfileData(BaseData): def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: sql = select(option).where( - and_(option.c.user == user_id, option.c.version == version) - ) + and_(option.c.user == user_id, option.c.version <= version) + ).order_by(option.c.version.desc()) result = self.execute(sql) if result is None: @@ -398,8 +400,8 @@ class Mai2ProfileData(BaseData): def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: sql = select(rating).where( - and_(rating.c.user == user_id, rating.c.version == version) - ) + and_(rating.c.user == user_id, rating.c.version <= version) + ).order_by(rating.c.version.desc()) result = self.execute(sql) if result is None: diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 4d3291d..0f7f239 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -25,6 +25,7 @@ best_score = Table( Column("syncStatus", Integer), Column("deluxscoreMax", Integer), Column("scoreRank", Integer), + Column("extNum1", Integer, server_default="0"), UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"), mysql_charset="utf8mb4", ) @@ -143,6 +144,7 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), + Column("extNum4", Integer, server_default="0"), Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index e40e37f..b22dcc4 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -16,6 +16,7 @@ event = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), mysql_charset="utf8mb4", @@ -108,7 +109,7 @@ class Mai2StaticData(BaseData): return None return result.fetchall() - def toggle_game_events( + def toggle_game_event( self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( @@ -118,7 +119,7 @@ class Mai2StaticData(BaseData): result = self.execute(sql) if result is None: self.logger.warning( - f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}" + f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}" ) return result.last_updated_params() diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 54fe896..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,7 +1,4 @@ -from typing import Any, List, Dict -from datetime import datetime, timedelta -import pytz -import json +from typing import Dict from core.config import CoreConfig from titles.mai2.universe import Mai2Universe From 68e25b9c5eebcefb87f70c8ee6cbcec67596584a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 11 Apr 2023 11:40:05 -0400 Subject: [PATCH 02/36] pokken: add frontend stub --- core/frontend.py | 7 ++--- titles/pokken/__init__.py | 8 +++--- titles/pokken/frontend.py | 33 +++++++++++++++++++++++ titles/pokken/frontend/pokken_index.jinja | 4 +++ 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 titles/pokken/frontend.py create mode 100644 titles/pokken/frontend/pokken_index.jinja diff --git a/core/frontend.py b/core/frontend.py index 127b174..0c96fdc 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -66,13 +66,14 @@ class FrontendServlet(resource.Resource): fe_game = FE_Game(cfg, self.environment) games = Utils.get_all_titles() for game_dir, game_mod in games.items(): - if hasattr(game_mod, "frontend"): + if hasattr(game_mod, "frontend"): try: game_fe = game_mod.frontend(cfg, self.environment, config_dir) self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) fe_game.putChild(game_dir.encode(), game_fe) - except: - raise + + except Exception as e: + self.logger.error(f"Failed to import frontend from {game_dir} because {e}") self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index ed2ee23..94237c4 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -1,8 +1,10 @@ -from titles.pokken.index import PokkenServlet -from titles.pokken.const import PokkenConstants -from titles.pokken.database import PokkenData +from .index import PokkenServlet +from .const import PokkenConstants +from .database import PokkenData +from .frontend import PokkenFrontend index = PokkenServlet database = PokkenData game_codes = [PokkenConstants.GAME_CODE] current_schema_version = 1 +frontend = PokkenFrontend diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py new file mode 100644 index 0000000..e4e8947 --- /dev/null +++ b/titles/pokken/frontend.py @@ -0,0 +1,33 @@ +import yaml +import jinja2 +from twisted.web.http import Request +from os import path + +from core.frontend import FE_Base +from core.config import CoreConfig +from .database import PokkenData +from .config import PokkenConfig +from .const import PokkenConstants + + +class PokkenFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = PokkenData(cfg) + self.game_cfg = PokkenConfig() + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) + self.nav_name = "Pokken" + + def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/pokken/frontend/pokken_index.jinja" + ) + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + ).encode("utf-16") diff --git a/titles/pokken/frontend/pokken_index.jinja b/titles/pokken/frontend/pokken_index.jinja new file mode 100644 index 0000000..446893a --- /dev/null +++ b/titles/pokken/frontend/pokken_index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Pokken

+{% endblock content %} \ No newline at end of file From 28c06335b648b9f02a374feca7b471bc3170712c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 11 Apr 2023 17:57:21 +0200 Subject: [PATCH 03/36] mai2: added upsert returns, fixed event reader, thanks @One3 Thanks to @One3 for helping with the events --- titles/mai2/base.py | 16 +++++++--- titles/mai2/read.py | 61 +++++++++++++++++++++++++++++++++++- titles/mai2/schema/item.py | 10 +++--- titles/mai2/schema/static.py | 2 +- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2e7198d..9f5c8af 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -98,16 +98,16 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) @@ -172,6 +172,8 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -186,6 +188,8 @@ class Mai2Base: datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), ) + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -300,8 +304,10 @@ class Mai2Base: ) self.data.item.put_friend_season_ranking(user_id, fsr) + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + def handle_user_logout_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 2c0567c..5809464 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -29,7 +29,7 @@ class Mai2Reader(BaseReader): f"Start importer for {Mai2Constants.game_ver_to_string(version)}" ) except IndexError: - self.logger.error(f"Invalid maidx version {version}") + self.logger.error(f"Invalid maimai DX version {version}") exit(1) def read(self) -> None: @@ -43,6 +43,7 @@ class Mai2Reader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") self.read_music(f"{dir}/music") self.read_tickets(f"{dir}/ticket") @@ -64,6 +65,64 @@ class Mai2Reader(BaseReader): ) self.logger.info(f"Added event {id}...") + def disable_events( + self, base_information_dir: str, base_score_ranking_dir: str + ) -> None: + self.logger.info(f"Reading disabled events from {base_information_dir}...") + + for root, dirs, files in os.walk(base_information_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Information.xml"): + with open(f"{root}/{dir}/Information.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("name").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + for root, dirs, files in os.walk(base_score_ranking_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/ScoreRanking.xml"): + with open(f"{root}/{dir}/ScoreRanking.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("eventName").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + # manually disable events wich are known to be problematic + for event_id in [ + 1, + 10, + 220311, + 220312, + 220313, + 220314, + 220315, + 220316, + 220317, + 220318, + 20121821, + 21121651, + 22091511, + 22091512, + 22091513, + 22091514, + 22091515, + 22091516, + 22091517, + 22091518, + 22091519, + ]: + self.data.static.toggle_game_event(self.version, event_id, toggle=False) + self.logger.info(f"Disabled event {event_id}...") + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index c6b2e7a..2dc5f5e 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -246,16 +246,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(login_bonus).values( user=user_id, - bonus_id=bonus_id, + bonusId=bonus_id, point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) result = self.execute(conflict) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index b22dcc4..76b163c 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -113,7 +113,7 @@ class Mai2StaticData(BaseData): self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( - and_(event.c.version == version, event.c.event_id == event_id) + and_(event.c.version == version, event.c.eventId == event_id) ).values(enabled=int(toggle)) result = self.execute(sql) From bd356af2726dfdc05f0278a23d866ba7d7b4b6c0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 12 Apr 2023 02:34:29 -0400 Subject: [PATCH 04/36] pokken: restructure database --- .gitignore | 1 + core/allnet.py | 2 +- titles/pokken/base.py | 7 ++++ titles/pokken/index.py | 5 ++- titles/pokken/schema/match.py | 5 ++- titles/pokken/schema/profile.py | 69 ++++++--------------------------- 6 files changed, 27 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 825729f..b5a0e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,5 +158,6 @@ cert/* !cert/server.pem config/* deliver/* +*.gz dbdump-*.json \ No newline at end of file diff --git a/core/allnet.py b/core/allnet.py index 0d4fbf7..b7cc4c6 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -55,7 +55,7 @@ class AllnetServlet: self.logger.error("No games detected!") for _, mod in plugins.items(): - if hasattr(mod.index, "get_allnet_info"): + if hasattr(mod, "index") and hasattr(mod.index, "get_allnet_info"): for code in mod.game_codes: enabled, uri, host = mod.index.get_allnet_info( code, self.config, self.config_folder diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 96d5efd..0e87397 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -17,6 +17,7 @@ class PokkenBase: self.version = 0 self.logger = logging.getLogger("pokken") self.data = PokkenData(core_cfg) + self.SUPPORT_SET_NONE = 4294967295 def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -281,6 +282,12 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG return res.SerializeToString() + def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_CHARGE + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index a7a328f..adfde88 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -87,7 +87,7 @@ class PokkenServlet(resource.Resource): if not game_cfg.server.enable: return (False, "") - return (True, "PKF2") + return (True, "PKF1") def setup(self) -> None: # TODO: Setup stun, turn (UDP) and admission (WSS) servers @@ -114,6 +114,8 @@ class PokkenServlet(resource.Resource): endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ pokken_request.type ].name.lower() + + self.logger.debug(pokken_request) handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: @@ -121,7 +123,6 @@ class PokkenServlet(resource.Resource): return self.base.handle_noop(pokken_request) self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") - self.logger.debug(pokken_request) ret = handler(pokken_request) return ret diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py index afe61f7..aec6bd3 100644 --- a/titles/pokken/schema/match.py +++ b/titles/pokken/schema/match.py @@ -14,8 +14,9 @@ match_data = Table( metadata, Column('id', Integer, primary_key=True, nullable=False), Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('play_mode', Integer), - Column('result', Integer), + Column('num_games', Integer), + Column('play_modes', JSON), + Column('results', JSON), Column('ex_ko_num', Integer), Column('wko_num', Integer), Column('timeup_win_num', Integer), diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index da6736c..bab53de 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -28,8 +28,10 @@ profile = Table( Column('score_point', Integer), Column('grade_max_num', Integer), Column('extra_counter', Integer), # Optional + Column('tutorial_progress_flag', JSON), # Repeated, Integer Column('total_play_days', Integer), Column('play_date_time', Integer), + Column('achievement_flag', JSON), # Repeated, Integer Column('lucky_box_fail_num', Integer), Column('event_reward_get_flag', Integer), Column('rank_pvp_all', Integer), @@ -41,15 +43,19 @@ profile = Table( Column('use_support_num', Integer), Column('rankmatch_flag', Integer), Column('rankmatch_max', Integer), # Optional + Column('rankmatch_progress', JSON), # Repeated, Integer Column('rankmatch_success', Integer), # Optional Column('beat_num', Integer), # Optional Column('title_text_id', Integer), Column('title_plate_id', Integer), Column('title_decoration_id', Integer), Column('support_pokemon_list', JSON), # Repeated, Integer - Column('support_set_1', JSON), # Repeated, Integer - Column('support_set_2', JSON), # Repeated, Integer - Column('support_set_3', JSON), # Repeated, Integer + Column('support_set_1_1', Integer), # Repeated, Integer + Column('support_set_1_2', Integer), + Column('support_set_2_1', Integer), # Repeated, Integer + Column('support_set_2_2', Integer), + Column('support_set_3_1', Integer), # Repeated, Integer + Column('support_set_3_2', Integer), Column('navi_trainer', Integer), Column('navi_version_id', Integer), Column('aid_skill_list', JSON), # Repeated, Integer @@ -84,56 +90,8 @@ profile = Table( Column('sp_bonus_category_id_2', Integer), Column('sp_bonus_key_value_2', Integer), Column('last_play_event_id', Integer), # Optional - mysql_charset="utf8mb4", -) - -tutorial_progress = Table( - 'pokken_tutorial_progress', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_tutorial_progress_uk'), - mysql_charset="utf8mb4", -) - -rankmatch_progress = Table( - 'pokken_rankmatch_progress', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('progress', Integer), - UniqueConstraint('user', 'progress', name='pokken_rankmatch_progress_uk'), - mysql_charset="utf8mb4", -) - -achievement_flag = Table( - 'pokken_achievement_flag', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_achievement_flag_uk'), - mysql_charset="utf8mb4", -) - -event_achievement_flag = Table( - 'pokken_event_achievement_flag', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('flag', Integer), - UniqueConstraint('user', 'flag', name='pokken_event_achievement_flag_uk'), - mysql_charset="utf8mb4", -) - -event_achievement_param = Table( - 'pokken_event_achievement_param', - metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('param', Integer), - UniqueConstraint('user', 'param', name='pokken_event_achievement_param_uk'), + Column('event_achievement_flag', JSON), # Repeated, Integer + Column('event_achievement_param', JSON), # Repeated, Integer mysql_charset="utf8mb4", ) @@ -176,10 +134,7 @@ class PokkenProfileData(BaseData): if result is None: self.logger.error(f"Failed to update pokken profile name for user {user_id}!") - def update_profile(self, user_id: int, profile_data: Dict) -> None: - """ - TODO: Find out what comes in on the SaveUserRequestData protobuf and save it! - """ + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass def get_profile(self, user_id: int) -> Optional[Row]: From 71c43a4a57475b236b52b11afef7e50a37285e2c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 12 Apr 2023 02:39:56 -0400 Subject: [PATCH 05/36] pokken: add_profile_points stub --- titles/pokken/schema/profile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index bab53de..37729fc 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -137,6 +137,9 @@ class PokkenProfileData(BaseData): def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass + def add_profile_points(self, user_id: int, rank_pts: int, money: int, score_pts: int) -> None: + pass + def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = self.execute(sql) From 97e3f1af0160a13b652d872b546018cfe3221460 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 13 Apr 2023 22:22:28 +0200 Subject: [PATCH 06/36] mai2: cardmaker festival support --- titles/cm/index.py | 7 ++++++- titles/cm/read.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/titles/cm/index.py b/titles/cm/index.py index d544e59..3a56566 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -12,6 +12,7 @@ from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig +from core.utils import Utils from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase @@ -82,6 +83,7 @@ class CardMakerServlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) print(f"version: {version}") @@ -107,7 +109,10 @@ class CardMakerServlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/cm/read.py b/titles/cm/read.py index f27b40b..93ff203 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -80,7 +80,7 @@ class CardMakerReader(BaseReader): for dir in data_dirs: self.read_chuni_card(f"{dir}/CHU/card") self.read_chuni_gacha(f"{dir}/CHU/gacha") - + self.read_mai2_card(f"{dir}/MAI/card") self.read_ongeki_gacha(f"{dir}/MU3/gacha") def read_chuni_card(self, base_dir: str) -> None: @@ -206,6 +206,7 @@ class CardMakerReader(BaseReader): "1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS, "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, + "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, } for root, dirs, files in os.walk(base_dir): From dc3e3e1fb34be7caed009ee6df7dd4ad570fdf4e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 14 Apr 2023 02:51:28 -0400 Subject: [PATCH 07/36] pokken: add constants, add stats to profile table --- titles/pokken/const.py | 12 +++++++ titles/pokken/schema/profile.py | 55 +++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 802a7b9..81b2ebc 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,3 +1,5 @@ +from enum import Enum + class PokkenConstants: GAME_CODE = "SDAK" @@ -7,6 +9,16 @@ class PokkenConstants: VERSION_NAMES = "Pokken Tournament" + class BATTLE_TYPE(Enum): + BATTLE_TYPE_TUTORIAL = 1 + BATTLE_TYPE_AI = 2 + BATTLE_TYPE_LAN = 3 + BATTLE_TYPE_WAN = 4 + + class BATTLE_RESULT(Enum): + BATTLE_RESULT_WIN = 1 + BATTLE_RESULT_LOSS = 2 + @classmethod def game_ver_to_string(cls, ver: int): return cls.VERSION_NAMES[ver] diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 37729fc..871ff9d 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from ..const import PokkenConstants # Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me # Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue @@ -67,7 +68,6 @@ profile = Table( Column('wko_num', Integer), Column('timeup_win_num', Integer), Column('cool_ko_num', Integer), - Column('cool_ko_num', Integer), Column('perfect_ko_num', Integer), Column('record_flag', Integer), Column('continue_num', Integer), @@ -92,7 +92,14 @@ profile = Table( Column('last_play_event_id', Integer), # Optional Column('event_achievement_flag', JSON), # Repeated, Integer Column('event_achievement_param', JSON), # Repeated, Integer - mysql_charset="utf8mb4", + Column('battle_num_vs_wan', Integer), # 4? + Column('win_vs_wan', Integer), + Column('battle_num_vs_lan', Integer), # 3? + Column('win_vs_lan', Integer), + Column('battle_num_vs_cpu', Integer), # 2 + Column('win_cpu', Integer), + Column('battle_num_tutorial', Integer), # 1? + mysql_charset="utf8mb4" ) pokemon_data = Table( @@ -101,20 +108,22 @@ pokemon_data = Table( Column('id', Integer, primary_key=True, nullable=False), Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column('char_id', Integer, nullable=False), - Column('illustration_book_no', Integer, nullable=False), - Column('pokemon_exp', Integer, nullable=False), - Column('battle_num_vs_wan', Integer, nullable=False), - Column('win_vs_wan', Integer, nullable=False), - Column('battle_num_vs_lan', Integer, nullable=False), - Column('win_vs_lan', Integer, nullable=False), - Column('battle_num_vs_cpu', Integer, nullable=False), - Column('win_cpu', Integer, nullable=False), - Column('battle_all_num_tutorial', Integer, nullable=False), - Column('battle_num_tutorial', Integer, nullable=False), - Column('bp_point_atk', Integer, nullable=False), - Column('bp_point_res', Integer, nullable=False), - Column('bp_point_def', Integer, nullable=False), - Column('bp_point_sp', Integer, nullable=False), + Column('illustration_book_no', Integer), + Column('pokemon_exp', Integer), + Column('battle_num_vs_wan', Integer), # 4? + Column('win_vs_wan', Integer), + Column('battle_num_vs_lan', Integer), # 3? + Column('win_vs_lan', Integer), + Column('battle_num_vs_cpu', Integer), # 2 + Column('win_cpu', Integer), + Column('battle_all_num_tutorial', Integer), + Column('battle_num_tutorial', Integer), # 1? + Column('bp_point_atk', Integer), + Column('bp_point_res', Integer), + Column('bp_point_def', Integer), + Column('bp_point_sp', Integer), + UniqueConstraint('user', 'char_id', name="pokken_pokemon_data_uk"), + mysql_charset="utf8mb4" ) class PokkenProfileData(BaseData): @@ -146,7 +155,7 @@ class PokkenProfileData(BaseData): if result is None: return None return result.fetchone() - def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: + def put_pokemon_data(self, user_id: int, pokemon_id: int, illust_no: int, get_exp: int, atk: int, res: int, defe: int, sp: int) -> Optional[int]: pass def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: @@ -154,3 +163,15 @@ class PokkenProfileData(BaseData): def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: pass + + def put_results(self, user_id: int, pokemon_id: int, match_type: int, match_result: int) -> None: + """ + Records the match stats (type and win/loss) for the pokemon and profile + """ + pass + + def put_stats(self, user_id: int, exkos: int, wkos: int, timeout_wins: int, cool_kos: int, perfects: int, continues: int) -> None: + """ + Records profile stats + """ + pass From b1f9be0121ac4ceb257a1cf1e7b4feb2f62add59 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 14 Apr 2023 23:47:31 -0400 Subject: [PATCH 08/36] wacca: fix crash on 4th page of Reverse Gate, partially fixes #5 --- core/data/schema/versions/SDFE_3_rollback.sql | 3 +++ core/data/schema/versions/SDFE_4_upgrade.sql | 3 +++ titles/wacca/__init__.py | 2 +- titles/wacca/base.py | 3 --- 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 core/data/schema/versions/SDFE_3_rollback.sql create mode 100644 core/data/schema/versions/SDFE_4_upgrade.sql diff --git a/core/data/schema/versions/SDFE_3_rollback.sql b/core/data/schema/versions/SDFE_3_rollback.sql new file mode 100644 index 0000000..b2310ad --- /dev/null +++ b/core/data/schema/versions/SDFE_3_rollback.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +--Can't really add items back so this just does nothing +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDFE_4_upgrade.sql b/core/data/schema/versions/SDFE_4_upgrade.sql new file mode 100644 index 0000000..03c5d1e --- /dev/null +++ b/core/data/schema/versions/SDFE_4_upgrade.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +DELETE FROM wacca_item WHERE type=17 AND item_id=312002; +SET FOREIGN_KEY_CHECKS=1; diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index b6e06f8..b3f9850 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -9,4 +9,4 @@ database = WaccaData reader = WaccaReader frontend = WaccaFrontend game_codes = [WaccaConstants.GAME_CODE] -current_schema_version = 3 +current_schema_version = 4 diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 020c167..bef7ee2 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -275,9 +275,6 @@ class WaccaBase: self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 ) # Added reverse - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002 - ) # Added reverse return UserStatusCreateResponseV2(profileId, req.username).make() From 0d5567c9900ff5454bded304b8e87fb58fa8e06e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:12:28 -0400 Subject: [PATCH 09/36] wacca: fix v4 upgrade scripts --- core/data/schema/versions/SDFE_3_rollback.sql | 1 - core/data/schema/versions/SDFE_4_upgrade.sql | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/data/schema/versions/SDFE_3_rollback.sql b/core/data/schema/versions/SDFE_3_rollback.sql index b2310ad..6d1d2b8 100644 --- a/core/data/schema/versions/SDFE_3_rollback.sql +++ b/core/data/schema/versions/SDFE_3_rollback.sql @@ -1,3 +1,2 @@ SET FOREIGN_KEY_CHECKS=0; ---Can't really add items back so this just does nothing SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDFE_4_upgrade.sql b/core/data/schema/versions/SDFE_4_upgrade.sql index 03c5d1e..3f2cc9a 100644 --- a/core/data/schema/versions/SDFE_4_upgrade.sql +++ b/core/data/schema/versions/SDFE_4_upgrade.sql @@ -1,3 +1 @@ -SET FOREIGN_KEY_CHECKS=0; -DELETE FROM wacca_item WHERE type=17 AND item_id=312002; -SET FOREIGN_KEY_CHECKS=1; +DELETE FROM wacca_item WHERE type=17 AND item_id=312002; \ No newline at end of file From baa885f674ffbb91a9bd192d8943569d41bf561b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:12:45 -0400 Subject: [PATCH 10/36] Utils: exclude malformed game folders --- core/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index d18289e..6bd5c08 100644 --- a/core/utils.py +++ b/core/utils.py @@ -16,7 +16,8 @@ class Utils: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - ret[dir] = mod + if hasattr(mod, "game_codes") and hasattr(mod, "index"): # Minimum required to function + ret[dir] = mod except ImportError as e: logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") From a416fb09e1ea3ff53e9d80f1f8af405f4890ea2b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 00:13:04 -0400 Subject: [PATCH 11/36] dbutils: version can now be left black to auto-upgrade to latest --- core/data/database.py | 27 +++++++++++++++++++++++---- dbutils.py | 11 ++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 07fe79e..c52e61c 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -32,7 +32,7 @@ class Data: self.arcade = ArcadeData(self.config, self.session) self.card = CardData(self.config, self.session) self.base = BaseData(self.config, self.session) - self.schema_ver_latest = 4 + self.current_schema_version = 4 log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" log_fmt = logging.Formatter(log_fmt_str) @@ -84,8 +84,8 @@ class Data: f"Could not load database schema from {game_dir} - {e}" ) - self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}") - self.base.set_schema_ver(self.schema_ver_latest) + self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}") + self.base.set_schema_ver(self.current_schema_version) self.logger.info( f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}" @@ -129,9 +129,28 @@ class Data: self.create_database() - def migrate_database(self, game: str, version: int, action: str) -> None: + def migrate_database(self, game: str, version: Optional[int], action: str) -> None: old_ver = self.base.get_schema_ver(game) sql = "" + if version is None: + if not game == "CORE": + titles = Utils.get_all_titles() + + for folder, mod in titles.items(): + for game_ in mod.game_codes: + if not game_ == game: continue + + if hasattr(mod, "current_schema_version"): + version = mod.current_schema_version + + else: + self.logger.warn(f"current_schema_version not found for {folder}") + + else: + version = self.current_schema_version + + if version is None: + self.logger.warn(f"Could not determine latest version for {game}, please specify --version") if old_ver is None: self.logger.error( diff --git a/dbutils.py b/dbutils.py index 176c67e..14af98e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,5 +1,6 @@ import yaml import argparse +import logging from core.config import CoreConfig from core.data import Data from os import path, mkdir, access, W_OK @@ -43,6 +44,7 @@ if __name__ == "__main__": ) exit(1) + cfg.update({"database": {"loglevel": "info"}}) # Force it to be info if we're doing db work data = Data(cfg) @@ -54,15 +56,14 @@ if __name__ == "__main__": elif args.action == "upgrade" or args.action == "rollback": if args.version is None: - data.logger.error("Must set game and version to migrate to") - exit(0) + data.logger.warn("No version set, upgrading to latest") if args.game is None: - data.logger.info("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version), args.action) + data.logger.warn("No game set, upgrading core schema") + data.migrate_database("CORE", int(args.version) if args.version is not None else None, args.action) else: - data.migrate_database(args.game, int(args.version), args.action) + data.migrate_database(args.game, int(args.version) if args.version is not None else None, args.action) elif args.action == "autoupgrade": data.autoupgrade() From 44193100866fa89bed24f5ffc7e773d142262d39 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 01:31:40 -0400 Subject: [PATCH 12/36] fix schema versions for diva and ongeki --- titles/diva/__init__.py | 2 +- titles/ongeki/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index 9d93468..3f193db 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -7,4 +7,4 @@ index = DivaServlet database = DivaData reader = DivaReader game_codes = [DivaConstants.GAME_CODE] -current_schema_version = 1 +current_schema_version = 4 diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index ddde049..1ba901b 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -7,4 +7,4 @@ index = OngekiServlet database = OngekiData reader = OngekiReader game_codes = [OngekiConstants.GAME_CODE] -current_schema_version = 2 +current_schema_version = 4 From 98950681253fe062ea8c84f51e1093dc8fda664f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 01:31:52 -0400 Subject: [PATCH 13/36] database: fix autoupdate --- core/data/database.py | 63 ++++++++++++++++++++++++++++------------ core/data/schema/base.py | 2 +- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index c52e61c..12eee8e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,5 +1,5 @@ import logging, coloredlogs -from typing import Optional +from typing import Optional, Dict, List from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine @@ -137,14 +137,13 @@ class Data: titles = Utils.get_all_titles() for folder, mod in titles.items(): - for game_ in mod.game_codes: - if not game_ == game: continue - - if hasattr(mod, "current_schema_version"): - version = mod.current_schema_version - - else: - self.logger.warn(f"current_schema_version not found for {folder}") + if not mod.game_codes[0] == game: continue + + if hasattr(mod, "current_schema_version"): + version = mod.current_schema_version + + else: + self.logger.warn(f"current_schema_version not found for {folder}") else: version = self.current_schema_version @@ -282,17 +281,45 @@ class Data: self.user.delete_user(user["id"]) def autoupgrade(self) -> None: - all_games = self.base.get_all_schema_vers() - if all_games is None: + all_game_versions = self.base.get_all_schema_vers() + if all_game_versions is None: self.logger.warn("Failed to get schema versions") + return - for x in all_games: + print(all_game_versions) + + all_games = Utils.get_all_titles() + all_games_list: Dict[str, int] = {} + for _, mod in all_games.items(): + if hasattr(mod, "current_schema_version"): + all_games_list[mod.game_codes[0]] = mod.current_schema_version + + for x in all_game_versions: + game = x["game"].upper() - update_ver = 1 - for y in range(2, 100): + update_ver = int(x["version"]) + latest_ver = all_games_list.get(game, 1) + if game == "CORE": + latest_ver = self.current_schema_version + + if update_ver == latest_ver: + self.logger.info(f"{game} is already latest version") + continue + + for y in range(update_ver + 1, latest_ver + 1): if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): - update_ver = y + with open( + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error(f"Error execuing sql script for game {game} v{y}!") + continue else: - break - - self.migrate_database(game, update_ver, "upgrade") \ No newline at end of file + self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + + self.base.set_schema_ver(latest_ver, game) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index f77a9aa..ea2fc88 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -47,7 +47,7 @@ class BaseData: res = None try: - self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())} || {opts}") + self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}") res = self.conn.execute(text(sql), opts) except SQLAlchemyError as e: From 83d2151b6bf36175805d94ab11bed748106339aa Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:06:11 -0400 Subject: [PATCH 14/36] dbutils: fix config loading incorrectly --- dbutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dbutils.py b/dbutils.py index 14af98e..3be3ebe 100644 --- a/dbutils.py +++ b/dbutils.py @@ -33,7 +33,9 @@ if __name__ == "__main__": cfg = CoreConfig() if path.exists(f"{args.config}/core.yaml"): - cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) + cfg_dict.get('database', {})['loglevel'] = 'info' + cfg.update(cfg_dict) if not path.exists(cfg.server.log_dir): mkdir(cfg.server.log_dir) @@ -44,7 +46,6 @@ if __name__ == "__main__": ) exit(1) - cfg.update({"database": {"loglevel": "info"}}) # Force it to be info if we're doing db work data = Data(cfg) From 4102ba21fcd1148a975b07eb29a8f641d1a879a2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:07:15 -0400 Subject: [PATCH 15/36] database: remove print --- core/data/database.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 12eee8e..bcfcb1c 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -286,8 +286,6 @@ class Data: self.logger.warn("Failed to get schema versions") return - print(all_game_versions) - all_games = Utils.get_all_titles() all_games_list: Dict[str, int] = {} for _, mod in all_games.items(): From 0dc96f33e110ce69c0495e29b0898ee59578c465 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 15 Apr 2023 03:13:14 -0400 Subject: [PATCH 16/36] database: don't set schema version if autoupdate fails --- core/data/database.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index bcfcb1c..40bbf33 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -293,7 +293,7 @@ class Data: all_games_list[mod.game_codes[0]] = mod.current_schema_version for x in all_game_versions: - + failed = False game = x["game"].upper() update_ver = int(x["version"]) latest_ver = all_games_list.get(game, 1) @@ -316,8 +316,10 @@ class Data: result = self.base.execute(sql) if result is None: self.logger.error(f"Error execuing sql script for game {game} v{y}!") - continue + failed = True + break else: self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + failed = True - self.base.set_schema_ver(latest_ver, game) + if not failed: self.base.set_schema_ver(latest_ver, game) From 469ead7a842f0a3920299f20468849ba45a3f2b2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 18 Apr 2023 02:52:41 -0400 Subject: [PATCH 17/36] wacca: add previously disabled gates, for #15 --- example_config/wacca.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example_config/wacca.yaml b/example_config/wacca.yaml index aea4f16..bd96795 100644 --- a/example_config/wacca.yaml +++ b/example_config/wacca.yaml @@ -29,5 +29,8 @@ gates: - 17 - 18 - 19 + - 20 - 21 - 22 + - 23 + - 24 From b0042bc776b2abcf0b4f44b2cd74aa2cc3b520bb Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 10:50:40 -0400 Subject: [PATCH 18/36] docs: update in accordance with new dbutils --- docs/game_specific_info.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index b09d61e..7700da5 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -64,8 +64,7 @@ which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to v perform all previous updates as well: ```shell -python dbutils.py --game SDBT --version 2 upgrade -python dbutils.py --game SDBT --version 3 upgrade +python dbutils.py --game SDBT upgrade ``` ## crossbeats REV. @@ -133,7 +132,7 @@ crash without it!** Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well: ```shell -python dbutils.py --game SDEZ --version 2 upgrade +python dbutils.py --game SDEZ upgrade ``` ## Hatsune Miku Project Diva @@ -174,9 +173,7 @@ which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to v perform all previous updates as well: ```shell -python dbutils.py --game SBZV --version 2 upgrade -python dbutils.py --game SBZV --version 3 upgrade -python dbutils.py --game SBZV --version 4 upgrade +python dbutils.py --game SBZV upgrade ``` ## O.N.G.E.K.I. @@ -224,9 +221,7 @@ which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to v perform all previous updates as well: ```shell -python dbutils.py --game SDDT --version 2 upgrade -python dbutils.py --game SDDT --version 3 upgrade -python dbutils.py --game SDDT --version 4 upgrade +python dbutils.py --game SDDT upgrade ``` ## Card Maker @@ -346,6 +341,5 @@ Config file is located in `config/wacca.yaml`. Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: ```shell -python dbutils.py --game SDFE --version 2 upgrade -python dbutils.py --game SDFE --version 3 upgrade +python dbutils.py --game SDFE upgrade ``` From 15433b681caea701a7611470c1509bc7e2d304b9 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 11:26:33 -0400 Subject: [PATCH 19/36] mai2: fix logging in put_card --- titles/mai2/schema/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d64d954..81e817a 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -438,7 +438,7 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: self.logger.warn( - f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}" + f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}" ) return None return result.lastrowid From 958471b8ebdcad0240bfddb947bd49ac725a0d86 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 19 Apr 2023 17:41:36 +0200 Subject: [PATCH 20/36] mai2: update script hotfix --- core/data/schema/versions/SDEZ_3_rollback.sql | 6 +++--- core/data/schema/versions/SDEZ_4_upgrade.sql | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql index f819855..79ca098 100644 --- a/core/data/schema/versions/SDEZ_3_rollback.sql +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -20,12 +20,12 @@ CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; ALTER TABLE mai2_item_friend_season_ranking -CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN seasonId season_id INT NOT NULL, CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonusId bonus_id INT NOT NULL, -CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, -CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file +CHANGE COLUMN isCurrent is_current BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql index 52d120a..a670d6a 100644 --- a/core/data/schema/versions/SDEZ_4_upgrade.sql +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -27,5 +27,5 @@ CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonus_id bonusId INT NOT NULL, -CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, -CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; +CHANGE COLUMN is_current isCurrent BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; From 68b0894e477b56bd41b19e0214305f365301ab50 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 19 Apr 2023 16:12:35 -0400 Subject: [PATCH 21/36] wacca: fix first play of the day calculation --- titles/wacca/base.py | 32 ++++++++++++-------------------- titles/wacca/lily.py | 28 +++++++--------------------- 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index bef7ee2..d4bf873 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -183,8 +183,6 @@ class WaccaBase: def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() - is_new_day = False - is_consec_day = False is_consec_day = True if req.userId == 0: @@ -202,29 +200,23 @@ class WaccaBase: self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time + midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int( - datetime.now() - .replace(hour=0, minute=0, second=0, microsecond=0) - .timestamp() - ): - is_new_day = True - is_consec_day = True - - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int( - ( - datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - + timedelta(days=1) - ).timestamp() - ): + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) + self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) + + if resp.firstLoginDaily: + # TODO: Daily bonus + pass - resp.firstLoginDaily = int(is_new_day) + # TODO: VIP dialy/monthly rewards return resp.make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index c3b6eb4..122cd47 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -143,8 +143,6 @@ class WaccaLily(WaccaS): def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() - is_new_day = False - is_consec_day = False is_consec_day = True if req.userId == 0: @@ -162,34 +160,22 @@ class WaccaLily(WaccaS): self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time + midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int( - datetime.now() - .replace(hour=0, minute=0, second=0, microsecond=0) - .timestamp() - ): - is_new_day = True - is_consec_day = True - - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int( - ( - datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - + timedelta(days=1) - ).timestamp() - ): + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) + self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month resp.vipInfo.pageDay = datetime.now().day resp.vipInfo.numItem = 1 - resp.firstLoginDaily = int(is_new_day) - return resp.make() def handle_user_status_getDetail_request(self, data: Dict) -> Dict: From 4d6afd757f3257bfb7b695f6765c9b21b481d1b8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 00:54:16 -0400 Subject: [PATCH 22/36] wacca: add helpers for gacha, event and friend info, fix settings not being applied correctly --- titles/wacca/base.py | 4 +- titles/wacca/handlers/helpers.py | 87 +++++++++++++++++++++------- titles/wacca/handlers/housing.py | 6 +- titles/wacca/handlers/user_status.py | 22 +++++-- titles/wacca/lily.py | 5 ++ 5 files changed, 93 insertions(+), 31 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index d4bf873..5663588 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -825,7 +825,7 @@ class WaccaBase: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - resp.songDetail.lock_state = 1 + resp.songDetail.lockState = 1 return resp.make() # TODO: Coop and vs data @@ -971,7 +971,7 @@ class WaccaBase: user_id = self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: - self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val) + self.data.profile.update_option(user_id, opt.optId, opt.optVal) for update in req.datesUpdated: pass diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index f86be2f..dc836ce 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -158,11 +158,11 @@ class Notice: class UserOption: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: - self.opt_id = opt_id - self.opt_val = opt_val + self.optId = opt_id + self.optVal = opt_val def make(self) -> List: - return [self.opt_id, self.opt_val] + return [self.optId, self.optVal] class UserStatusV1: @@ -348,13 +348,35 @@ class NavigatorItem(IconItem): class SkillItem: - skill_type: int + skillType: int level: int flag: int badge: int def make(self) -> List: - return [self.skill_type, self.level, self.flag, self.badge] + return [self.skillType, self.level, self.flag, self.badge] + + +class UserEventInfo: + def __init__(self) -> None: + self.eventId = 0 + self.conditionInfo: List[UserEventConditionInfo] = [] + + def make(self) -> List: + conditions = [] + for x in self.conditionInfo: + conditions.append(x.make()) + + return [self.eventId, conditions] + + +class UserEventConditionInfo: + def __init__(self) -> None: + self.achievementCondition = 0 + self.progress = 0 + + def make(self) -> List: + return [self.achievementCondition, self.progress] class UserItemInfoV1: @@ -447,19 +469,19 @@ class UserItemInfoV3(UserItemInfoV2): class SongDetailClearCounts: def __init__( self, - play_ct: int = 0, - clear_ct: int = 0, - ml_ct: int = 0, - fc_ct: int = 0, - am_ct: int = 0, + playCt: int = 0, + clearCt: int = 0, + mlCt: int = 0, + fcCt: int = 0, + amCt: int = 0, counts: Optional[List[int]] = None, ) -> None: if counts is None: - self.playCt = play_ct - self.clearCt = clear_ct - self.misslessCt = ml_ct - self.fullComboCt = fc_ct - self.allMarvelousCt = am_ct + self.playCt = playCt + self.clearCt = clearCt + self.misslessCt = mlCt + self.fullComboCt = fcCt + self.allMarvelousCt = amCt else: self.playCt = counts[0] @@ -773,8 +795,12 @@ class GateDetailV2(GateDetailV1): class GachaInfo: + def __init__(self, gacha_id: int = 0, gacha_roll_ct: int = 0) -> None: + self.gachaId = gacha_id + self.rollCt = gacha_roll_ct + def make(self) -> List: - return [] + return [self.gachaId, self.rollCt] class LastSongDetail: @@ -808,9 +834,30 @@ class LastSongDetail: ] -class FriendDetail: +class FriendScoreDetail: + def __init__(self, song_id: int = 0, difficulty: int = 1, best_score: int = 0) -> None: + self.songId = song_id + self.difficulty = difficulty + self.bestScore = best_score + def make(self) -> List: - return [] + return [self.songId, self.difficulty, self.bestScore] + + +class FriendDetail: + def __init__(self, user_id: int = 0, username: str = "") -> None: + self.friendId = user_id + self.friendUsername = username + self.friendUserType = 1 + self.friendScoreDetail: List[FriendScoreDetail] = [] + + def make(self) -> List: + scores = [] + + for x in self.friendScoreDetail: + scores.append(x.make()) + + return [self.friendId, self.friendUsername, self.friendUserType, scores] class LoginBonusInfo: @@ -942,7 +989,7 @@ class MusicUpdateDetailV1: self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.lock_state = 0 + self.lockState = 0 def make(self) -> List: return [ @@ -954,7 +1001,7 @@ class MusicUpdateDetailV1: self.score, self.lowestMissCount, self.maxSkillPts, - self.lock_state, + self.lockState, ] diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 8ffa910..f2f079e 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -10,10 +10,10 @@ class HousingGetResponse(BaseResponse): def __init__(self, housingId: int) -> None: super().__init__() self.housingId: int = housingId - self.regionId: int = 0 + self.isNewCab: bool = False def make(self) -> Dict: - self.params = [self.housingId, self.regionId] + self.params = [self.housingId, int(self.isNewCab)] return super().make() @@ -32,8 +32,6 @@ class HousingStartRequestV1(BaseRequest): class HousingStartRequestV2(HousingStartRequestV1): def __init__(self, data: Dict) -> None: super(HousingStartRequestV1, self).__init__(data) - self.unknown0: str = self.params[0] - self.errorLog: str = self.params[1] self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 0e3819d..b09fed6 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -39,12 +39,16 @@ class UserStatusGetV2Response(UserStatusGetV1Response): def __init__(self) -> None: super().__init__() self.userStatus: UserStatusV2 = UserStatusV2() - self.unknownArr: List = [] + self.options: List[UserOption] = [] def make(self) -> Dict: super().make() + opts = [] - self.params.append(self.unknownArr) + for x in self.options: + opts.append(x.make()) + + self.params.append(opts) return super(UserStatusGetV1Response, self).make() @@ -137,7 +141,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): self.userItems: UserItemInfoV2 = UserItemInfoV2() self.favorites: List[int] = [] self.stoppedSongIds: List[int] = [] - self.eventInfo: List[int] = [] + self.eventInfo: List[UserEventInfo] = [] self.gateInfo: List[GateDetailV1] = [] self.lastSongInfo: LastSongDetail = LastSongDetail() self.gateTutorialFlags: List[GateTutorialFlag] = [] @@ -149,6 +153,8 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): gates = [] friends = [] tut_flg = [] + evts = [] + gacha = [] for x in self.gateInfo: gates.append(x.make()) @@ -162,14 +168,20 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) + + for x in self.eventInfo: + evts.append(x.make()) + + for x in self.gatchaInfo: + gacha.append(x.make()) self.params.append(self.favorites) self.params.append(self.stoppedSongIds) - self.params.append(self.eventInfo) + self.params.append(evts) self.params.append(gates) self.params.append(self.lastSongInfo.make()) self.params.append(tut_flg) - self.params.append(self.gatchaInfo) + self.params.append(gacha) self.params.append(friends) return super(UserStatusGetDetailResponseV1, self).make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 122cd47..4ff942b 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -73,6 +73,8 @@ class WaccaLily(WaccaS): self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() + + opts = self.data.profile.get_options(req.aimeId) self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: @@ -137,6 +139,9 @@ class WaccaLily(WaccaS): if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 + + for opt in opts: + resp.options.append(UserOption(opt["opt_id"], opt["value"])) return resp.make() From 241f29e29c25506b23e43acf46338ea87057a1f7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 02:02:42 -0400 Subject: [PATCH 23/36] wacca: add a comment --- titles/wacca/handlers/user_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index b09fed6..b7ed1fc 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -267,7 +267,7 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2): self, is_first_login_daily: bool = False, last_login_date: int = 0 ) -> None: super().__init__(is_first_login_daily, last_login_date) - self.unk: List = [] + self.unk: List = [] # Ticket info, item info, message, title, voice name (not sure how they fit...) def make(self) -> Dict: super().make() From 00b127361b35a301e1817d7a00760f5f35c0b9ac Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 02:27:09 -0400 Subject: [PATCH 24/36] wacca: enable time play --- titles/wacca/reverse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index f32b0c4..ef14ecb 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -137,6 +137,9 @@ class WaccaReverse(WaccaLilyR): PlayModeCounts(self.season, 4, profile["playcount_stageup"]) ) + # For some fucking reason if this isn't here time play is disabled + resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) From a30967e8d79ea0defeb06dcacfb62571d525ff3f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 20 Apr 2023 09:46:18 -0400 Subject: [PATCH 25/36] wacca: fix time free not saving, add counter to profile table --- core/data/schema/versions/SDFE_4_rollback.sql | 1 + core/data/schema/versions/SDFE_5_upgrade.sql | 1 + titles/wacca/__init__.py | 2 +- titles/wacca/handlers/helpers.py | 1 + titles/wacca/schema/profile.py | 13 +++++++++---- 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 core/data/schema/versions/SDFE_4_rollback.sql create mode 100644 core/data/schema/versions/SDFE_5_upgrade.sql diff --git a/core/data/schema/versions/SDFE_4_rollback.sql b/core/data/schema/versions/SDFE_4_rollback.sql new file mode 100644 index 0000000..417c227 --- /dev/null +++ b/core/data/schema/versions/SDFE_4_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile DROP COLUMN playcount_time_free; \ No newline at end of file diff --git a/core/data/schema/versions/SDFE_5_upgrade.sql b/core/data/schema/versions/SDFE_5_upgrade.sql new file mode 100644 index 0000000..a9795bf --- /dev/null +++ b/core/data/schema/versions/SDFE_5_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup; \ No newline at end of file diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index b3f9850..a3bf96b 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -9,4 +9,4 @@ database = WaccaData reader = WaccaReader frontend = WaccaFrontend game_codes = [WaccaConstants.GAME_CODE] -current_schema_version = 4 +current_schema_version = 5 diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index dc836ce..b17602a 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -941,6 +941,7 @@ class PlayType(Enum): PlayTypeVs = 2 PlayTypeCoop = 3 PlayTypeStageup = 4 + PlayTypeTimeFree = 5 class StageInfo: diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index 27111be..48eb800 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from ..handlers.helpers import PlayType profile = Table( "wacca_profile", @@ -40,6 +41,7 @@ profile = Table( Column("playcount_multi_vs", Integer, server_default="0"), Column("playcount_multi_coop", Integer, server_default="0"), Column("playcount_stageup", Integer, server_default="0"), + Column("playcount_time_free", Integer, server_default="0"), Column("friend_view_1", Integer), Column("friend_view_2", Integer), Column("friend_view_3", Integer), @@ -160,17 +162,20 @@ class WaccaProfileData(BaseData): ) -> None: sql = profile.update(profile.c.id == profile_id).values( playcount_single=profile.c.playcount_single + 1 - if play_type == 1 + if play_type == PlayType.PlayTypeSingle.value else profile.c.playcount_single, playcount_multi_vs=profile.c.playcount_multi_vs + 1 - if play_type == 2 + if play_type == PlayType.PlayTypeVs.value else profile.c.playcount_multi_vs, playcount_multi_coop=profile.c.playcount_multi_coop + 1 - if play_type == 3 + if play_type == PlayType.PlayTypeCoop.value else profile.c.playcount_multi_coop, playcount_stageup=profile.c.playcount_stageup + 1 - if play_type == 4 + if play_type == PlayType.PlayTypeStageup.value else profile.c.playcount_stageup, + playcount_time_free=profile.c.playcount_time_free + 1 + if play_type == PlayType.PlayTypeTimeFree.value + else profile.c.playcount_time_free, last_game_ver=game_version, ) From 190c41e03e31d7e8e703c80bacf341e745b26ae9 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 21 Apr 2023 10:51:30 -0400 Subject: [PATCH 26/36] wacca: added play mode counter for time free --- titles/wacca/reverse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index ef14ecb..bb8a332 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -136,6 +136,9 @@ class WaccaReverse(WaccaLilyR): resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 4, profile["playcount_stageup"]) ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 5, profile["playcount_time_free"]) + ) # For some fucking reason if this isn't here time play is disabled resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) From 58a088b9a47a3da34cfe9f2aead6605732a2f21d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 21 Apr 2023 10:51:44 -0400 Subject: [PATCH 27/36] wacca: add debug log for ticket use --- titles/wacca/base.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 5663588..35a6efc 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -634,6 +634,7 @@ class WaccaBase: elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets: for x in range(len(new_tickets)): if new_tickets[x][1] == item.itemId: + self.logger.debug(f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}") self.data.item.spend_ticket(new_tickets[x][0]) new_tickets.pop(x) break @@ -657,13 +658,8 @@ class WaccaBase: ) if self.game_config.mods.infinite_tickets: - new_tickets = [ - [0, 106002, 0], - [1, 106002, 0], - [2, 106002, 0], - [3, 106002, 0], - [4, 106002, 0], - ] + for x in range(5): + new_tickets.append(TicketItem(x, 106002, 0)) if self.game_config.mods.infinite_wp: current_wp = 999999 From d8c3ed5c013c96001da2e931c7419564356c4bd7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 04:38:28 -0400 Subject: [PATCH 28/36] Add support for initial d zero --- example_config/idz.yaml | 11 ++ titles/idz/__init__.py | 8 + titles/idz/config.py | 67 ++++++++ titles/idz/const.py | 40 +++++ titles/idz/database.py | 6 + titles/idz/echo.py | 16 ++ titles/idz/handlers/__init__.py | 39 +++++ titles/idz/handlers/base.py | 25 +++ titles/idz/handlers/check_team_names.py | 19 ++ titles/idz/handlers/create_auto_team.py | 49 ++++++ titles/idz/handlers/create_profile.py | 91 ++++++++++ titles/idz/handlers/create_team.py | 0 titles/idz/handlers/discover_profile.py | 23 +++ titles/idz/handlers/load_2on2.py | 36 ++++ titles/idz/handlers/load_config.py | 41 +++++ titles/idz/handlers/load_ghost.py | 39 +++++ titles/idz/handlers/load_profile.py | 28 +++ titles/idz/handlers/load_reward_table.py | 17 ++ titles/idz/handlers/load_server_info.py | 60 +++++++ titles/idz/handlers/load_team_ranking.py | 29 ++++ titles/idz/handlers/load_top_ten.py | 30 ++++ titles/idz/handlers/lock_profile.py | 39 +++++ titles/idz/handlers/save_expedition.py | 14 ++ titles/idz/handlers/save_profile.py | 15 ++ titles/idz/handlers/save_topic.py | 17 ++ titles/idz/handlers/unknown.py | 12 ++ .../handlers/update_provisional_store_rank.py | 20 +++ titles/idz/handlers/update_story_clear_num.py | 26 +++ titles/idz/index.py | 146 ++++++++++++++++ titles/idz/match.py | 0 titles/idz/userdb.py | 162 ++++++++++++++++++ 31 files changed, 1125 insertions(+) create mode 100644 example_config/idz.yaml create mode 100644 titles/idz/__init__.py create mode 100644 titles/idz/config.py create mode 100644 titles/idz/const.py create mode 100644 titles/idz/database.py create mode 100644 titles/idz/echo.py create mode 100644 titles/idz/handlers/__init__.py create mode 100644 titles/idz/handlers/base.py create mode 100644 titles/idz/handlers/check_team_names.py create mode 100644 titles/idz/handlers/create_auto_team.py create mode 100644 titles/idz/handlers/create_profile.py create mode 100644 titles/idz/handlers/create_team.py create mode 100644 titles/idz/handlers/discover_profile.py create mode 100644 titles/idz/handlers/load_2on2.py create mode 100644 titles/idz/handlers/load_config.py create mode 100644 titles/idz/handlers/load_ghost.py create mode 100644 titles/idz/handlers/load_profile.py create mode 100644 titles/idz/handlers/load_reward_table.py create mode 100644 titles/idz/handlers/load_server_info.py create mode 100644 titles/idz/handlers/load_team_ranking.py create mode 100644 titles/idz/handlers/load_top_ten.py create mode 100644 titles/idz/handlers/lock_profile.py create mode 100644 titles/idz/handlers/save_expedition.py create mode 100644 titles/idz/handlers/save_profile.py create mode 100644 titles/idz/handlers/save_topic.py create mode 100644 titles/idz/handlers/unknown.py create mode 100644 titles/idz/handlers/update_provisional_store_rank.py create mode 100644 titles/idz/handlers/update_story_clear_num.py create mode 100644 titles/idz/index.py create mode 100644 titles/idz/match.py create mode 100644 titles/idz/userdb.py diff --git a/example_config/idz.yaml b/example_config/idz.yaml new file mode 100644 index 0000000..1bfff9b --- /dev/null +++ b/example_config/idz.yaml @@ -0,0 +1,11 @@ +server: + enable: True + loglevel: "info" + hostname: "" + news: "" + aes_key: "" + +ports: + userdb: 10000 + match: 10010 + echo: 10020 diff --git a/titles/idz/__init__.py b/titles/idz/__init__.py new file mode 100644 index 0000000..958d08a --- /dev/null +++ b/titles/idz/__init__.py @@ -0,0 +1,8 @@ +from titles.idz.index import IDZServlet +from titles.idz.const import IDZConstants +from titles.idz.database import IDZData + +index = IDZServlet +database = IDZData +game_codes = [IDZConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idz/config.py b/titles/idz/config.py new file mode 100644 index 0000000..2ff4e5c --- /dev/null +++ b/titles/idz/config.py @@ -0,0 +1,67 @@ +from typing import List, Dict + +from core.config import CoreConfig + + +class IDZServerConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "idz", "server", "loglevel", default="info" + ) + ) + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "hostname", default="" + ) + + @property + def news(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "news", default="" + ) + + @property + def aes_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "aes_key", default="" + ) + + +class IDZPortsConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def userdb(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "userdb", default=10000) + + @property + def match(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "match", default=10010) + + @property + def echo(self) -> int: + return CoreConfig.get_config_field(self.__config, "idz", "ports", "echo", default=10020) + + +class IDZConfig(dict): + def __init__(self) -> None: + self.server = IDZServerConfig(self) + self.ports = IDZPortsConfig(self) + + @property + def rsa_keys(self) -> List[Dict]: + return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) \ No newline at end of file diff --git a/titles/idz/const.py b/titles/idz/const.py new file mode 100644 index 0000000..164c940 --- /dev/null +++ b/titles/idz/const.py @@ -0,0 +1,40 @@ +from enum import Enum + +class IDZConstants: + GAME_CODE = "SDDF" + + CONFIG_NAME = "idz.yaml" + + VER_IDZ_110 = 0 + VER_IDZ_130 = 1 + VER_IDZ_210 = 2 + VER_IDZ_230 = 3 + NUM_VERS = 4 + + VERSION_NAMES = ( + "Initial D Arcade Stage Zero v1.10", + "Initial D Arcade Stage Zero v1.30", + "Initial D Arcade Stage Zero v2.10", + "Initial D Arcade Stage Zero v2.30", + ) + + class PROFILE_STATUS(Enum): + LOCKED = 0 + UNLOCKED = 1 + OLD = 2 + + HASH_LUT = [ + # No clue + 0x9C82E674, 0x5A4738D9, 0x8B8D7AE0, 0x29EC9D81, + # These three are from AES TE0 + 0x1209091B, 0x1D83839E, 0x582C2C74, 0x341A1A2E, + 0x361B1B2D, 0xDC6E6EB2, 0xB45A5AEE, 0x5BA0A0FB, + 0xA45252F6, 0x763B3B4D, 0xB7D6D661, 0x7DB3B3CE, + ] + HASH_NUM = 0 + HASH_MUL = [5, 7, 11, 12][HASH_NUM] + HASH_XOR = [0xB3, 0x8C, 0x14, 0x50][HASH_NUM] + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/idz/database.py b/titles/idz/database.py new file mode 100644 index 0000000..1f5a8e2 --- /dev/null +++ b/titles/idz/database.py @@ -0,0 +1,6 @@ +from core.data import Data +from core.config import CoreConfig + +class IDZData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) diff --git a/titles/idz/echo.py b/titles/idz/echo.py new file mode 100644 index 0000000..4414b70 --- /dev/null +++ b/titles/idz/echo.py @@ -0,0 +1,16 @@ +from twisted.internet.protocol import DatagramProtocol +import logging + +from core.config import CoreConfig +from .config import IDZConfig + +class IDZEcho(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idz") + + def datagramReceived(self, data, addr): + self.logger.debug(f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}") + self.transport.write(data, addr) \ No newline at end of file diff --git a/titles/idz/handlers/__init__.py b/titles/idz/handlers/__init__.py new file mode 100644 index 0000000..a3cc248 --- /dev/null +++ b/titles/idz/handlers/__init__.py @@ -0,0 +1,39 @@ +from .base import IDZHandlerBase + +from .load_server_info import IDZHandlerLoadServerInfo + +from .load_ghost import IDZHandlerLoadGhost + +from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB + +from .load_top_ten import IDZHandlerLoadTopTen + +from .update_story_clear_num import IDZHandlerUpdateStoryClearNum + +from .save_expedition import IDZHandlerSaveExpedition + +from .load_2on2 import IDZHandlerLoad2on2A, IDZHandlerLoad2on2B + +from .load_team_ranking import IDZHandlerLoadTeamRankingA, IDZHandlerLoadTeamRankingB + +from .discover_profile import IDZHandlerDiscoverProfile + +from .lock_profile import IDZHandlerLockProfile + +from .check_team_names import IDZHandlerCheckTeamName + +from .unknown import IDZHandlerUnknown + +from .create_profile import IDZHandlerCreateProfile + +from .create_auto_team import IDZHandlerCreateAutoTeam + +from .load_profile import IDZHandlerLoadProfile + +from .save_profile import IDZHandlerSaveProfile + +from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank + +from .load_reward_table import IDZHandlerLoadRewardTable + +from .save_topic import IDZHandlerSaveTopic diff --git a/titles/idz/handlers/base.py b/titles/idz/handlers/base.py new file mode 100644 index 0000000..720f097 --- /dev/null +++ b/titles/idz/handlers/base.py @@ -0,0 +1,25 @@ +import logging +import struct +from core.data import Data +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerBase(): + name = "generic" + cmd_codes = [0x0000] * IDZConstants.NUM_VERS + rsp_codes = [0x0001] * IDZConstants.NUM_VERS + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + self.core_config = core_cfg + self.game_cfg = game_cfg + self.data = Data(core_cfg) + self.logger = logging.getLogger("idz") + self.game = IDZConstants.GAME_CODE + self.version = version + self.size = 0x30 + + def handle(self, data: bytes) -> bytearray: + ret = bytearray([0] * self.size) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ca0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + + aime_id = struct.unpack_from(" 2: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + user_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04c0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x1290 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + +class IDZHandlerLoad2on2B(IDZHandlerBase): + cmd_codes = [0x0132] * 4 + rsp_codes = [0x0133] * 4 + name = "load_2on2B" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04c0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x0540 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py new file mode 100644 index 0000000..fa103af --- /dev/null +++ b/titles/idz/handlers/load_config.py @@ -0,0 +1,41 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerLoadConfigA(IDZHandlerBase): + cmd_codes = [0x0004] * IDZConstants.NUM_VERS + rsp_codes = [0x0005] * IDZConstants.NUM_VERS + name = "load_config_a" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01a0 + + if self.version > 1: + self.size = 0x05e0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0230 + + if self.version > 1: + self.size = 0x0240 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0070 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0d30 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x0ea0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x1360 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x1640 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01c0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py new file mode 100644 index 0000000..3b60f00 --- /dev/null +++ b/titles/idz/handlers/load_server_info.py @@ -0,0 +1,60 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerLoadServerInfo(IDZHandlerBase): + cmd_codes = [0x0006] * IDZConstants.NUM_VERS + rsp_codes = [0x0007] * IDZConstants.NUM_VERS + name = "load_server_info1" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04b0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + offset = 0 + if self.version >= IDZConstants.VER_IDZ_210: + offset = 2 + + news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" + err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" + + len_hostname = len(self.core_config.title.hostname) + len_news = len(news_str) + len_error = len(err_str) + + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ba0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + +class IDZHandlerLoadTeamRankingB(IDZHandlerBase): + cmd_codes = [0x00bb, 0x00bb, 0x00a9, 0x00a9] + rsp_codes = [0x00a8] * 4 + name = "load_team_ranking_b" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0ba0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_top_ten.py b/titles/idz/handlers/load_top_ten.py new file mode 100644 index 0000000..ef2a158 --- /dev/null +++ b/titles/idz/handlers/load_top_ten.py @@ -0,0 +1,30 @@ +import struct +from typing import Tuple, List, Dict + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerLoadTopTen(IDZHandlerBase): + cmd_codes = [0x012c] * 4 + rsp_codes = [0x00ce] * 4 + name = "load_top_ten" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x1720 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + tracks_dates: List[Tuple[int, int]] = [] + for i in range(32): + tracks_dates.append( + (struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + profile_data = { + "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, + "expire_time": int((datetime.now() + timedelta(hours=1)).timestamp() / 1000) + } + user_id = struct.unpack_from(" 0: + old_profile = None + if old_profile is not None: + profile_data["status"] = IDZConstants.PROFILE_STATUS.OLD.value + + return self.handle_common(profile_data, ret) + + def handle_common(cls, data: Dict, ret: bytearray) -> bytearray: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_profile.py b/titles/idz/handlers/save_profile.py new file mode 100644 index 0000000..652dbd6 --- /dev/null +++ b/titles/idz/handlers/save_profile.py @@ -0,0 +1,15 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerSaveProfile(IDZHandlerBase): + cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143] + name = "save_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_topic.py b/titles/idz/handlers/save_topic.py new file mode 100644 index 0000000..9499385 --- /dev/null +++ b/titles/idz/handlers/save_topic.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerSaveTopic(IDZHandlerBase): + cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091] + rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092] + name = "save_topic" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x05d0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/unknown.py b/titles/idz/handlers/unknown.py new file mode 100644 index 0000000..ee3046a --- /dev/null +++ b/titles/idz/handlers/unknown.py @@ -0,0 +1,12 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerUnknown(IDZHandlerBase): + cmd_codes = [0x00ad, 0x00ad, 0x00a2, 0x00a2] + name = "unknown" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) diff --git a/titles/idz/handlers/update_provisional_store_rank.py b/titles/idz/handlers/update_provisional_store_rank.py new file mode 100644 index 0000000..cbe344d --- /dev/null +++ b/titles/idz/handlers/update_provisional_store_rank.py @@ -0,0 +1,20 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + +class IDZHandlerUpdateProvisionalStoreRank(IDZHandlerBase): + cmd_codes = [0x0082, 0x0082, 0x007C, 0x007C] + rsp_codes = [0x0083, 0x0083, 0x007D, 0x007D] + name = "update_provisional_store_ranking" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x02b0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + + def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray: + pass diff --git a/titles/idz/handlers/update_story_clear_num.py b/titles/idz/handlers/update_story_clear_num.py new file mode 100644 index 0000000..46f2689 --- /dev/null +++ b/titles/idz/handlers/update_story_clear_num.py @@ -0,0 +1,26 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerUpdateStoryClearNum(IDZHandlerBase): + cmd_codes = [0x007f, 0x097f, 0x013d, 0x0144] + rsp_codes = [0x0080, 0x013e, 0x013e, 0x0145] + name = "update_story_clear_num" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0220 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x04f0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x0510 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x0800 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/index.py b/titles/idz/index.py new file mode 100644 index 0000000..5b60e5a --- /dev/null +++ b/titles/idz/index.py @@ -0,0 +1,146 @@ +from twisted.web.http import Request +import yaml +import logging +import coloredlogs +from logging.handlers import TimedRotatingFileHandler +from os import path +from typing import Tuple, List +from twisted.internet import reactor, endpoints +from twisted.web import server, resource +import importlib + +from core.config import CoreConfig +from .config import IDZConfig +from .const import IDZConstants +from .userdb import IDZUserDBFactory, IDZUserDBWeb, IDZKey +from .echo import IDZEcho +from .handlers import IDZHandlerLoadConfigB + +class IDZServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + self.logger = logging.getLogger("idz") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] IDZ | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idz"), + encoding="utf8", + when="d", + backupCount=10, + ) + + self.rsa_keys: List[IDZKey] = [] + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + @classmethod + def rsaHashKeyN(cls, data): + hash_ = 0 + for i in data: + hash_ = hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) ^ IDZConstants.HASH_LUT[i & 0xf] + hash_ &= 0xffffffff + return hash_ + + @classmethod + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "", "") + + if len(game_cfg.rsa_keys) <= 0 or not game_cfg.server.aes_key: + logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start") + return (False, "", "") + + hostname = core_cfg.title.hostname if not game_cfg.server.hostname else game_cfg.server.hostname + return ( + True, + f"", + f"{hostname}:{game_cfg.ports.userdb}", + ) + + def setup(self): + for key in self.game_cfg.rsa_keys: + if "N" not in key or "d" not in key or "e" not in key: + self.logger.error(f"Invalid IDZ key {key}") + continue + + hashN = self.rsaHashKeyN(str(key["N"]).encode()) + self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN)) + + if len(self.rsa_keys) <= 0: + self.logger.error("No valid RSA keys provided! IDZ cannot start!") + return + + handler_map = [{} for _ in range(IDZConstants.NUM_VERS)] + handler_mod = mod = importlib.import_module(f"titles.idz.handlers") + + for cls_name in dir(handler_mod): + if cls_name.startswith("__"): + continue + + try: + mod = getattr(handler_mod, cls_name) + mod_cmds: List = getattr(mod, "cmd_codes") + while len(mod_cmds) < IDZConstants.NUM_VERS: + mod_cmds.append(None) + + for i in range(len(mod_cmds)): + if mod_cmds[i] is None: + mod_cmds[i] = mod_cmds[i - 1] + + handler_map[i][mod_cmds[i]] = mod + + except AttributeError as e: + continue + + endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}")\ + .listen(IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map)) + + reactor.listenUDP(self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg)) + reactor.listenUDP(self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg)) + + self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") + + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + req_raw = request.content.getvalue() + self.logger.info(f"IDZ POST request: {url_path} - {req_raw}") + return b"" + + def render_GET(self, request: Request, version: int, url_path: str) -> bytes: + self.logger.info(f"IDZ GET request: {url_path}") + request.responseHeaders.setRawHeaders('Content-Type', [b"text/plain; charset=utf-8"]) + request.responseHeaders.setRawHeaders("Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"]) + + news = self.game_cfg.server.news if self.game_cfg.server.news else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + news += "\r\n" + news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news + + return news.encode() diff --git a/titles/idz/match.py b/titles/idz/match.py new file mode 100644 index 0000000..e69de29 diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py new file mode 100644 index 0000000..95a2eb9 --- /dev/null +++ b/titles/idz/userdb.py @@ -0,0 +1,162 @@ +from twisted.internet.protocol import Factory, Protocol +import logging, coloredlogs +from Crypto.Cipher import AES +import struct +from typing import Dict, Optional, List, Type +from twisted.web import server, resource +from twisted.internet import reactor, endpoints +from twisted.web.http import Request +from routes import Mapper +import random +from os import walk +import importlib + +from core.config import CoreConfig +from .database import IDZData +from .config import IDZConfig +from .const import IDZConstants +from .handlers import IDZHandlerBase + +HANDLER_MAP: List[Dict] + +class IDZKey: + def __init__(self, n, d, e, hashN: int) -> None: + self.N = n + self.d = d + self.e = e + self.hashN = hashN + +class IDZUserDBProtocol(Protocol): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, keys: List[IDZKey], handlers: List[Dict]) -> None: + self.logger = logging.getLogger('idz') + self.core_config = core_cfg + self.game_config = game_cfg + self.rsa_keys = keys + self.handlers = handlers + self.static_key = bytes.fromhex(self.game_config.server.aes_key) + self.version = None + self.version_internal = None + self.skip_next = False + + def append_padding(self, data: bytes): + """Appends 0s to the end of the data until it's at the correct size""" + length = struct.unpack_from(" None: + self.logger.debug(f"{self.transport.getPeer().host} Connected") + base = 0 + + for i in range(len(self.static_key) - 1): + shift = 8 * i + byte = self.static_key[i] + + base |= byte << shift + + rsa_key = random.choice(self.rsa_keys) + key_enc: int = pow(base, rsa_key.e, rsa_key.N) + result = key_enc.to_bytes(0x40, "little") + struct.pack(" None: + self.logger.debug( + f"{self.transport.getPeer().host} Disconnected - {reason.value}" + ) + + def dataReceived(self, data: bytes) -> None: + self.logger.debug(f"Receive data {data.hex()}") + crypt = AES.new(self.static_key, AES.MODE_ECB) + data_dec = crypt.decrypt(data) + self.logger.debug(f"Decrypt data {data_dec.hex()}") + + magic = struct.unpack_from(" None: + self.core_config = cfg + self.game_config = game_cfg + self.keys = keys + self.handlers = handlers + + def buildProtocol(self, addr): + return IDZUserDBProtocol(self.core_config, self.game_config, self.keys, self.handlers) + +class IDZUserDBWeb(resource.Resource): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): + super().__init__() + self.isLeaf = True + self.core_config = core_cfg + self.game_config = game_cfg + self.logger = logging.getLogger('idz') + + def render_POST(self, request: Request) -> bytes: + self.logger.info(f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}") + return b"" + + def render_GET(self, request: Request) -> bytes: + self.logger.info(f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}") + return b"" \ No newline at end of file From 26c4bcb466555910fa0832906163816b1c28dea2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 13:26:44 -0400 Subject: [PATCH 29/36] idz: Add requests, fix load_config_b --- titles/idz/handlers/__init__.py | 8 ++++++++ titles/idz/handlers/load_config.py | 2 +- titles/idz/handlers/save_time_attack.py | 22 ++++++++++++++++++++++ titles/idz/handlers/unlock_profile.py | 20 ++++++++++++++++++++ titles/idz/handlers/update_team_points.py | 17 +++++++++++++++++ titles/idz/handlers/update_ui_report.py | 17 +++++++++++++++++ titles/idz/handlers/update_user_log.py | 17 +++++++++++++++++ 7 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 titles/idz/handlers/save_time_attack.py create mode 100644 titles/idz/handlers/unlock_profile.py create mode 100644 titles/idz/handlers/update_team_points.py create mode 100644 titles/idz/handlers/update_ui_report.py create mode 100644 titles/idz/handlers/update_user_log.py diff --git a/titles/idz/handlers/__init__.py b/titles/idz/handlers/__init__.py index a3cc248..17213ba 100644 --- a/titles/idz/handlers/__init__.py +++ b/titles/idz/handlers/__init__.py @@ -37,3 +37,11 @@ from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank from .load_reward_table import IDZHandlerLoadRewardTable from .save_topic import IDZHandlerSaveTopic + +from .save_time_attack import IDZHandlerSaveTimeAttack + +from .unlock_profile import IDZHandlerUnlockProfile + +from .update_team_points import IDZHandleUpdateTeamPoints + +from .update_ui_report import IDZHandleUpdateUIReport diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py index fa103af..3ae57f5 100644 --- a/titles/idz/handlers/load_config.py +++ b/titles/idz/handlers/load_config.py @@ -24,7 +24,7 @@ class IDZHandlerLoadConfigA(IDZHandlerBase): return ret class IDZHandlerLoadConfigB(IDZHandlerBase): - cmd_codes = [0x00a0] * IDZConstants.NUM_VERS + cmd_codes = [0x00ab, 0x00ab, 0x00a0, 0x00a0] rsp_codes = [0x00ac, 0x00ac, 0x00a1, 0x00a1] name = "load_config_b" diff --git a/titles/idz/handlers/save_time_attack.py b/titles/idz/handlers/save_time_attack.py new file mode 100644 index 0000000..a28a598 --- /dev/null +++ b/titles/idz/handlers/save_time_attack.py @@ -0,0 +1,22 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerSaveTimeAttack(IDZHandlerBase): + cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136] + rsp_codes = [0x00ce, 0x00ce, 0x00cd, 0x00cd] + name = "save_time_attack" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x00b0 + + if self.version > IDZConstants.VER_IDZ_130: + self.size = 0x00f0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret \ No newline at end of file diff --git a/titles/idz/handlers/unlock_profile.py b/titles/idz/handlers/unlock_profile.py new file mode 100644 index 0000000..a5c8310 --- /dev/null +++ b/titles/idz/handlers/unlock_profile.py @@ -0,0 +1,20 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandlerUnlockProfile(IDZHandlerBase): + cmd_codes = [0x006f, 0x006f, 0x006b, 0x006b] + rsp_codes = [0x0070, 0x0070, 0x006c, 0x006c] + name = "unlock_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_ui_report.py b/titles/idz/handlers/update_ui_report.py new file mode 100644 index 0000000..b77b4ea --- /dev/null +++ b/titles/idz/handlers/update_ui_report.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandleUpdateUIReport(IDZHandlerBase): + cmd_codes = [0x0084, 0x0084, 0x007e, 0x007e] + name = "update_ui_report" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_user_log.py b/titles/idz/handlers/update_user_log.py new file mode 100644 index 0000000..78679eb --- /dev/null +++ b/titles/idz/handlers/update_user_log.py @@ -0,0 +1,17 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + +class IDZHandleUpdateUserLog(IDZHandlerBase): + cmd_codes = [0x00bd, 0x00bd, 0x00ab, 0x00b3] + name = "update_user_log" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret From 47f4aaddf8742224600ecc99fafe34b3686565ad Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:00:30 -0400 Subject: [PATCH 30/36] allnet: add download order infrastructure --- core/allnet.py | 29 ++++++++++++++++++++++++++--- core/config.py | 5 +++++ index.py | 17 +++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index b7cc4c6..5c10735 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -10,6 +10,7 @@ from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 from time import strptime +from os import path from core.config import CoreConfig from core.utils import Utils @@ -191,12 +192,34 @@ class AllnetServlet: self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}") resp = AllnetDownloadOrderResponse() - if not self.config.allnet.allow_online_updates: + if not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder: return self.dict_to_http_form_string([vars(resp)]) - else: # TODO: Actual dlorder response + else: # TODO: Keychip check + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-app.ini" + + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): + resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-opt.ini" + + self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) + def handle_dlorder_ini(self, request:Request, match: Dict) -> bytes: + if "file" not in match: return b"" + + req_file = match['file'].replace('%0A', '') + + if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): + return open(f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb").read() + + self.logger.info(f"DL INI File {req_file} not found") + return b"" + + def handle_dlorder_report(self, request:Request, match: Dict) -> bytes: + self.logger.info(f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}") + return b"" + def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) request_ip = Utils.get_ip_addr(request) @@ -419,7 +442,7 @@ class AllnetDownloadOrderRequest: class AllnetDownloadOrderResponse: - def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: + def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None: self.stat = stat self.serial = serial self.uri = uri diff --git a/core/config.py b/core/config.py index 9e152c0..84311af 100644 --- a/core/config.py +++ b/core/config.py @@ -188,6 +188,11 @@ class AllnetConfig: self.__config, "core", "allnet", "allow_online_updates", default=False ) + @property + def update_cfg_folder(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "update_cfg_folder", default="" + ) class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: diff --git a/index.py b/index.py index be48ac4..1e1193a 100644 --- a/index.py +++ b/index.py @@ -25,6 +25,22 @@ class HttpDispatcher(resource.Resource): self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir) + + self.map_get.connect( + "allnet_downloadorder_ini", + "/dl/ini/{file}", + controller="allnet", + action="handle_dlorder_ini", + conditions=dict(method=["GET"]), + ) + + self.map_post.connect( + "allnet_downloadorder_report", + "/dl/report", + controller="allnet", + action="handle_dlorder_report", + conditions=dict(method=["POST"]), + ) self.map_post.connect( "allnet_ping", @@ -93,6 +109,7 @@ class HttpDispatcher(resource.Resource): conditions=dict(method=["POST"]), requirements=dict(game=R"S..."), ) + def render_GET(self, request: Request) -> bytes: self.logger.debug(request.uri) From 0668488ccf8bdcc48f4dde782b226887e169868e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:06:44 -0400 Subject: [PATCH 31/36] update core example config --- example_config/core.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/example_config/core.yaml b/example_config/core.yaml index 561293c..382c51b 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -32,6 +32,7 @@ allnet: loglevel: "info" port: 80 allow_online_updates: False + update_cfg_folder: "" billing: port: 8443 From b498e82bf862d57c30ff9e5fab11bceea5b1906e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 19:08:45 -0400 Subject: [PATCH 32/36] dli: remove dot from version --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 5c10735..07340a2 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -197,10 +197,10 @@ class AllnetServlet: else: # TODO: Keychip check if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-app.ini" + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): - resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver}-opt.ini" + resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) From f4ee4238d94beea5c0cfddd43bb452dca0aefd40 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 20:51:23 -0400 Subject: [PATCH 33/36] update readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 02f0c67..b1cb506 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + Lily R + Reverse ++ Pokken + + Final Online ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) From 9d23d59e43857e913ac60c834be5d77726fc22f6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 21:04:15 -0400 Subject: [PATCH 34/36] add changelog --- changelog.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e1b5642 --- /dev/null +++ b/changelog.md @@ -0,0 +1,49 @@ +# Changelog +Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. + +## 2023042300 +### Wacca ++ Time free now works properly ++ Fix reverse gate mission causing a fatal error ++ Other misc. fixes ++ Latest DB: 5 + +### Pokken ++ Added preliminary support + + Nothing saves currently, but the game will boot and function properly. + +### Initial D Zero ++ Added preliminary support + + Nothing saves currently, but the game will boot and function for the most part. + +### Mai2 ++ Added support for Festival ++ Lasted DB Version: 4 + +### Ongeki ++ Misc fixes ++ Lasted DB Version: 4 + +### Diva ++ Misc fixes ++ Lasted DB Version: 4 + +### Chuni ++ Fix network encryption ++ Add `handle_remove_token_api_request` for event mode + +### Allnet ++ Added download order support + + It is up to the sysop to provide the INI file, and host the files. + + ONLY for use with cabs. It's not checked currently, which it's why it's default disabled + + YMMV, use at your own risk ++ When running develop mode, games that are not recognised will still be able to authenticate. + +### Database ++ Add autoupgrade command + + Invoke to automatically upgrade all schemas to their latest versions + ++ `version` arg no longer required, leave it blank to update the game schema to latest if it isn't already + +### Misc ++ Update example nginx config file From 238d437519dc0ca621cc1614ab0043134de60ac0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Apr 2023 21:04:52 -0400 Subject: [PATCH 35/36] reformat with black in preperation for merge to master --- core/allnet.py | 50 ++-- core/config.py | 1 + core/data/database.py | 52 ++-- core/data/schema/base.py | 2 +- core/frontend.py | 8 +- core/mucha.py | 12 +- core/utils.py | 12 +- dbutils.py | 19 +- index.py | 3 +- read.py | 3 +- titles/chuni/index.py | 52 ++-- titles/chuni/newplus.py | 8 +- titles/chuni/schema/static.py | 4 +- titles/cm/index.py | 4 +- titles/cm/read.py | 2 +- titles/cxb/index.py | 4 +- titles/idz/config.py | 14 +- titles/idz/const.py | 23 +- titles/idz/database.py | 1 + titles/idz/echo.py | 7 +- titles/idz/handlers/base.py | 7 +- titles/idz/handlers/check_team_names.py | 7 +- titles/idz/handlers/create_auto_team.py | 30 +- titles/idz/handlers/create_profile.py | 48 +-- titles/idz/handlers/discover_profile.py | 7 +- titles/idz/handlers/load_2on2.py | 14 +- titles/idz/handlers/load_config.py | 18 +- titles/idz/handlers/load_ghost.py | 7 +- titles/idz/handlers/load_profile.py | 11 +- titles/idz/handlers/load_reward_table.py | 5 +- titles/idz/handlers/load_server_info.py | 72 +++-- titles/idz/handlers/load_team_ranking.py | 18 +- titles/idz/handlers/load_top_ten.py | 19 +- titles/idz/handlers/lock_profile.py | 19 +- titles/idz/handlers/save_expedition.py | 5 +- titles/idz/handlers/save_profile.py | 3 +- titles/idz/handlers/save_time_attack.py | 11 +- titles/idz/handlers/save_topic.py | 5 +- titles/idz/handlers/unknown.py | 3 +- titles/idz/handlers/unlock_profile.py | 9 +- .../handlers/update_provisional_store_rank.py | 9 +- titles/idz/handlers/update_story_clear_num.py | 11 +- titles/idz/handlers/update_team_points.py | 7 +- titles/idz/handlers/update_ui_report.py | 7 +- titles/idz/handlers/update_user_log.py | 7 +- titles/idz/index.py | 72 +++-- titles/idz/userdb.py | 79 +++-- titles/mai2/base.py | 4 +- titles/mai2/const.py | 2 +- titles/mai2/index.py | 6 +- titles/mai2/schema/item.py | 2 +- titles/mai2/schema/profile.py | 40 ++- titles/ongeki/base.py | 6 +- titles/ongeki/bright.py | 6 +- titles/ongeki/schema/item.py | 4 +- titles/pokken/base.py | 200 +++++++------ titles/pokken/config.py | 1 + titles/pokken/const.py | 1 + titles/pokken/database.py | 1 + titles/pokken/index.py | 33 ++- titles/pokken/schema/item.py | 21 +- titles/pokken/schema/match.py | 38 ++- titles/pokken/schema/profile.py | 278 ++++++++++-------- titles/pokken/schema/static.py | 3 +- titles/wacca/base.py | 48 ++- titles/wacca/handlers/helpers.py | 10 +- titles/wacca/handlers/user_info.py | 2 +- titles/wacca/handlers/user_status.py | 6 +- titles/wacca/lily.py | 18 +- titles/wacca/reverse.py | 2 +- 70 files changed, 920 insertions(+), 603 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 07340a2..119f0ae 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -105,9 +105,11 @@ class AllnetServlet: resp.stat = 0 return self.dict_to_http_form_string([vars(resp)]) - + else: - self.logger.info(f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}") + self.logger.info( + f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" + ) resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" return self.dict_to_http_form_string([vars(resp)]) @@ -189,35 +191,49 @@ class AllnetServlet: self.logger.error(e) return b"" - self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}") + self.logger.info( + f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" + ) resp = AllnetDownloadOrderResponse() - - if not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder: + + if ( + not self.config.allnet.allow_online_updates + or not self.config.allnet.update_cfg_folder + ): return self.dict_to_http_form_string([vars(resp)]) else: # TODO: Keychip check - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"): + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" + ): resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" - - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"): + + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" + ): resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" - + self.logger.debug(f"Sending download uri {resp.uri}") return self.dict_to_http_form_string([vars(resp)]) - def handle_dlorder_ini(self, request:Request, match: Dict) -> bytes: - if "file" not in match: return b"" + def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: + if "file" not in match: + return b"" + + req_file = match["file"].replace("%0A", "") - req_file = match['file'].replace('%0A', '') - if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): - return open(f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb").read() - + return open( + f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" + ).read() + self.logger.info(f"DL INI File {req_file} not found") return b"" - def handle_dlorder_report(self, request:Request, match: Dict) -> bytes: - self.logger.info(f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}") + def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: + self.logger.info( + f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}" + ) return b"" def handle_billing_request(self, request: Request, _: Dict): diff --git a/core/config.py b/core/config.py index 84311af..3fb0dbe 100644 --- a/core/config.py +++ b/core/config.py @@ -194,6 +194,7 @@ class AllnetConfig: self.__config, "core", "allnet", "update_cfg_folder", default="" ) + class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config diff --git a/core/data/database.py b/core/data/database.py index 40bbf33..719d05e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -71,7 +71,9 @@ class Data: games = Utils.get_all_titles() for game_dir, game_mod in games.items(): try: - if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"): + if hasattr(game_mod, "database") and hasattr( + game_mod, "current_schema_version" + ): game_mod.database(self.config) metadata.create_all(self.__engine.connect()) @@ -135,21 +137,26 @@ class Data: if version is None: if not game == "CORE": titles = Utils.get_all_titles() - + for folder, mod in titles.items(): - if not mod.game_codes[0] == game: continue - + if not mod.game_codes[0] == game: + continue + if hasattr(mod, "current_schema_version"): version = mod.current_schema_version - + else: - self.logger.warn(f"current_schema_version not found for {folder}") - + self.logger.warn( + f"current_schema_version not found for {folder}" + ) + else: version = self.current_schema_version - + if version is None: - self.logger.warn(f"Could not determine latest version for {game}, please specify --version") + self.logger.warn( + f"Could not determine latest version for {game}, please specify --version" + ) if old_ver is None: self.logger.error( @@ -184,7 +191,7 @@ class Data: if result is None: self.logger.error("Error execuing sql script!") return None - + else: for x in range(old_ver, version, -1): if not os.path.exists( @@ -285,13 +292,13 @@ class Data: if all_game_versions is None: self.logger.warn("Failed to get schema versions") return - + all_games = Utils.get_all_titles() all_games_list: Dict[str, int] = {} for _, mod in all_games.items(): if hasattr(mod, "current_schema_version"): all_games_list[mod.game_codes[0]] = mod.current_schema_version - + for x in all_game_versions: failed = False game = x["game"].upper() @@ -299,27 +306,30 @@ class Data: latest_ver = all_games_list.get(game, 1) if game == "CORE": latest_ver = self.current_schema_version - - if update_ver == latest_ver: + + if update_ver == latest_ver: self.logger.info(f"{game} is already latest version") continue - + for y in range(update_ver + 1, latest_ver + 1): if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): with open( - f"core/data/schema/versions/{game}_{y}_upgrade.sql", - "r", - encoding="utf-8", + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", ) as f: sql = f.read() result = self.base.execute(sql) if result is None: - self.logger.error(f"Error execuing sql script for game {game} v{y}!") + self.logger.error( + f"Error execuing sql script for game {game} v{y}!" + ) failed = True break else: self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") failed = True - - if not failed: self.base.set_schema_ver(latest_ver, game) + + if not failed: + self.base.set_schema_ver(latest_ver, game) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index ea2fc88..7957301 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -81,7 +81,7 @@ class BaseData: Generate a random 5-7 digit id """ return randrange(10000, 9999999) - + def get_all_schema_vers(self) -> Optional[List[Row]]: sql = select(schema_ver) diff --git a/core/frontend.py b/core/frontend.py index 0c96fdc..c992e76 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -66,14 +66,16 @@ class FrontendServlet(resource.Resource): fe_game = FE_Game(cfg, self.environment) games = Utils.get_all_titles() for game_dir, game_mod in games.items(): - if hasattr(game_mod, "frontend"): + if hasattr(game_mod, "frontend"): try: game_fe = game_mod.frontend(cfg, self.environment, config_dir) self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) fe_game.putChild(game_dir.encode(), game_fe) - + except Exception as e: - self.logger.error(f"Failed to import frontend from {game_dir} because {e}") + self.logger.error( + f"Failed to import frontend from {game_dir} because {e}" + ) self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) diff --git a/core/mucha.py b/core/mucha.py index 9dfef03..a90ab53 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -46,9 +46,7 @@ class MuchaServlet: if enabled: self.mucha_registry.append(game_cd) - self.logger.info( - f"Serving {len(self.mucha_registry)} games" - ) + self.logger.info(f"Serving {len(self.mucha_registry)} games") def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) @@ -62,9 +60,7 @@ class MuchaServlet: req = MuchaAuthRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info( - f"Boardauth request from {client_ip} for {req.gameVer}" - ) + self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") @@ -92,9 +88,7 @@ class MuchaServlet: req = MuchaUpdateRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info( - f"Updatecheck request from {client_ip} for {req.gameVer}" - ) + self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") diff --git a/core/utils.py b/core/utils.py index 6bd5c08..f364785 100644 --- a/core/utils.py +++ b/core/utils.py @@ -16,14 +16,20 @@ class Utils: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - if hasattr(mod, "game_codes") and hasattr(mod, "index"): # Minimum required to function + if hasattr(mod, "game_codes") and hasattr( + mod, "index" + ): # Minimum required to function ret[dir] = mod except ImportError as e: logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") raise return ret - + @classmethod def get_ip_addr(cls, req: Request) -> str: - return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host + return ( + req.getAllHeaders()[b"x-forwarded-for"].decode() + if b"x-forwarded-for" in req.getAllHeaders() + else req.getClientAddress().host + ) diff --git a/dbutils.py b/dbutils.py index 3be3ebe..d959232 100644 --- a/dbutils.py +++ b/dbutils.py @@ -34,12 +34,12 @@ if __name__ == "__main__": cfg = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) - cfg_dict.get('database', {})['loglevel'] = 'info' + cfg_dict.get("database", {})["loglevel"] = "info" cfg.update(cfg_dict) - + if not path.exists(cfg.server.log_dir): mkdir(cfg.server.log_dir) - + if not access(cfg.server.log_dir, W_OK): print( f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" @@ -47,7 +47,6 @@ if __name__ == "__main__": exit(1) data = Data(cfg) - if args.action == "create": data.create_database() @@ -61,10 +60,18 @@ if __name__ == "__main__": if args.game is None: data.logger.warn("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version) if args.version is not None else None, args.action) + data.migrate_database( + "CORE", + int(args.version) if args.version is not None else None, + args.action, + ) else: - data.migrate_database(args.game, int(args.version) if args.version is not None else None, args.action) + data.migrate_database( + args.game, + int(args.version) if args.version is not None else None, + args.action, + ) elif args.action == "autoupgrade": data.autoupgrade() diff --git a/index.py b/index.py index 1e1193a..11fad94 100644 --- a/index.py +++ b/index.py @@ -25,7 +25,7 @@ class HttpDispatcher(resource.Resource): self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir) - + self.map_get.connect( "allnet_downloadorder_ini", "/dl/ini/{file}", @@ -109,7 +109,6 @@ class HttpDispatcher(resource.Resource): conditions=dict(method=["POST"]), requirements=dict(game=R"S..."), ) - def render_GET(self, request: Request) -> bytes: self.logger.debug(request.uri) diff --git a/read.py b/read.py index a1bd0ab..14c5cc2 100644 --- a/read.py +++ b/read.py @@ -135,8 +135,7 @@ if __name__ == "__main__": for dir, mod in titles.items(): if args.series in mod.game_codes: - handler = mod.reader(config, args.version, - bin_arg, opt_arg, args.extra) + handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) handler.read() logger.info("Done") diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 53db19f..a7545ba 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -82,21 +82,33 @@ class ChuniServlet: level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) self.logger.inited = True - + for version, keys in self.game_cfg.crypto.keys.items(): if len(keys) < 3: continue - + self.hash_table[version] = {} - - method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')] + + method_list = [ + method + for method in dir(self.versions[version]) + if not method.startswith("__") + ] for method in method_list: method_fixed = inflection.camelize(method)[6:-7] - hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1) - + hash = PBKDF2( + method_fixed, + bytes.fromhex(keys[2]), + 128, + count=44, + hmac_hash_module=SHA1, + ) + self.hash_table[version][hash.hex()] = method_fixed - - self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}") + + self.logger.debug( + f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" + ) @classmethod def get_allnet_info( @@ -164,18 +176,22 @@ class ChuniServlet: # technically not 0 if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: endpoint = request.getHeader("User-Agent").split("#")[0] - + else: if internal_ver not in self.hash_table: - self.logger.error(f"v{version} does not support encryption or no keys entered") + self.logger.error( + f"v{version} does not support encryption or no keys entered" + ) return zlib.compress(b'{"stat": "0"}') - + elif endpoint.lower() not in self.hash_table[internal_ver]: - self.logger.error(f"No hash found for v{version} endpoint {endpoint}") + self.logger.error( + f"No hash found for v{version} endpoint {endpoint}" + ) return zlib.compress(b'{"stat": "0"}') endpoint = self.hash_table[internal_ver][endpoint.lower()] - + try: crypt = AES.new( bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), @@ -193,7 +209,11 @@ class ChuniServlet: encrtped = True - if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: + if ( + not encrtped + and self.game_cfg.crypto.encrypted_only + and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + ): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) @@ -210,9 +230,7 @@ class ChuniServlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 422d57a..4faf47a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -13,8 +13,12 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] - ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[ + "rom" + ] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[ + "data" + ] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index bef58c0..4537518 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -200,7 +200,9 @@ class ChuniStaticData(BaseData): return result.lastrowid def get_login_bonus( - self, version: int, preset_id: int, + self, + version: int, + preset_id: int, ) -> Optional[List[Row]]: sql = login_bonus.select( and_( diff --git a/titles/cm/index.py b/titles/cm/index.py index 3a56566..74d3a0d 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -109,9 +109,7 @@ class CardMakerServlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/cm/read.py b/titles/cm/read.py index 93ff203..109483c 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -90,7 +90,7 @@ class CardMakerReader(BaseReader): "v2_00": ChuniConstants.VER_CHUNITHM_NEW, "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, # Chunithm SUN, ignore for now - "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1 + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1, } for root, dirs, files in os.walk(base_dir): diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 36c762e..0c38d55 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -101,9 +101,7 @@ class CxbServlet(resource.Resource): f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}" ) else: - self.logger.info( - f"Ready on port {self.game_cfg.server.port}" - ) + self.logger.info(f"Ready on port {self.game_cfg.server.port}") def render_POST(self, request: Request): version = 0 diff --git a/titles/idz/config.py b/titles/idz/config.py index 2ff4e5c..f7af4fd 100644 --- a/titles/idz/config.py +++ b/titles/idz/config.py @@ -46,15 +46,21 @@ class IDZPortsConfig: @property def userdb(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "userdb", default=10000) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "userdb", default=10000 + ) @property def match(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "match", default=10010) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "match", default=10010 + ) @property def echo(self) -> int: - return CoreConfig.get_config_field(self.__config, "idz", "ports", "echo", default=10020) + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "echo", default=10020 + ) class IDZConfig(dict): @@ -64,4 +70,4 @@ class IDZConfig(dict): @property def rsa_keys(self) -> List[Dict]: - return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) \ No newline at end of file + return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) diff --git a/titles/idz/const.py b/titles/idz/const.py index 164c940..93cbb4c 100644 --- a/titles/idz/const.py +++ b/titles/idz/const.py @@ -1,5 +1,6 @@ from enum import Enum + class IDZConstants: GAME_CODE = "SDDF" @@ -22,14 +23,26 @@ class IDZConstants: LOCKED = 0 UNLOCKED = 1 OLD = 2 - + HASH_LUT = [ # No clue - 0x9C82E674, 0x5A4738D9, 0x8B8D7AE0, 0x29EC9D81, + 0x9C82E674, + 0x5A4738D9, + 0x8B8D7AE0, + 0x29EC9D81, # These three are from AES TE0 - 0x1209091B, 0x1D83839E, 0x582C2C74, 0x341A1A2E, - 0x361B1B2D, 0xDC6E6EB2, 0xB45A5AEE, 0x5BA0A0FB, - 0xA45252F6, 0x763B3B4D, 0xB7D6D661, 0x7DB3B3CE, + 0x1209091B, + 0x1D83839E, + 0x582C2C74, + 0x341A1A2E, + 0x361B1B2D, + 0xDC6E6EB2, + 0xB45A5AEE, + 0x5BA0A0FB, + 0xA45252F6, + 0x763B3B4D, + 0xB7D6D661, + 0x7DB3B3CE, ] HASH_NUM = 0 HASH_MUL = [5, 7, 11, 12][HASH_NUM] diff --git a/titles/idz/database.py b/titles/idz/database.py index 1f5a8e2..525b1c1 100644 --- a/titles/idz/database.py +++ b/titles/idz/database.py @@ -1,6 +1,7 @@ from core.data import Data from core.config import CoreConfig + class IDZData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) diff --git a/titles/idz/echo.py b/titles/idz/echo.py index 4414b70..979fd19 100644 --- a/titles/idz/echo.py +++ b/titles/idz/echo.py @@ -4,6 +4,7 @@ import logging from core.config import CoreConfig from .config import IDZConfig + class IDZEcho(DatagramProtocol): def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: super().__init__() @@ -12,5 +13,7 @@ class IDZEcho(DatagramProtocol): self.logger = logging.getLogger("idz") def datagramReceived(self, data, addr): - self.logger.debug(f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}") - self.transport.write(data, addr) \ No newline at end of file + self.logger.debug( + f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" + ) + self.transport.write(data, addr) diff --git a/titles/idz/handlers/base.py b/titles/idz/handlers/base.py index 720f097..6b1e5d5 100644 --- a/titles/idz/handlers/base.py +++ b/titles/idz/handlers/base.py @@ -5,7 +5,8 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants -class IDZHandlerBase(): + +class IDZHandlerBase: name = "generic" cmd_codes = [0x0000] * IDZConstants.NUM_VERS rsp_codes = [0x0001] * IDZConstants.NUM_VERS @@ -18,8 +19,8 @@ class IDZHandlerBase(): self.game = IDZConstants.GAME_CODE self.version = version self.size = 0x30 - + def handle(self, data: bytes) -> bytearray: ret = bytearray([0] * self.size) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ca0 - + self.size = 0x0CA0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0020 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" 2: struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) user_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04c0 + self.size = 0x04C0 if version >= IDZConstants.VER_IDZ_210: self.size = 0x1290 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) + class IDZHandlerLoad2on2B(IDZHandlerBase): cmd_codes = [0x0132] * 4 rsp_codes = [0x0133] * 4 @@ -27,10 +29,10 @@ class IDZHandlerLoad2on2B(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04c0 + self.size = 0x04C0 if version >= IDZConstants.VER_IDZ_210: self.size = 0x0540 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py index 3ae57f5..b3ceb0d 100644 --- a/titles/idz/handlers/load_config.py +++ b/titles/idz/handlers/load_config.py @@ -5,6 +5,7 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerLoadConfigA(IDZHandlerBase): cmd_codes = [0x0004] * IDZConstants.NUM_VERS rsp_codes = [0x0005] * IDZConstants.NUM_VERS @@ -12,29 +13,30 @@ class IDZHandlerLoadConfigA(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x01a0 - + self.size = 0x01A0 + if self.version > 1: - self.size = 0x05e0 - + self.size = 0x05E0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0230 - + if self.version > 1: self.size = 0x0240 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0070 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) if self.version == IDZConstants.VER_IDZ_110: - self.size = 0x0d30 + self.size = 0x0D30 elif self.version == IDZConstants.VER_IDZ_130: - self.size = 0x0ea0 + self.size = 0x0EA0 elif self.version == IDZConstants.VER_IDZ_210: self.size = 0x1360 elif self.version == IDZConstants.VER_IDZ_230: self.size = 0x1640 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) aime_id = struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x01c0 - + self.size = 0x01C0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index 3b60f00..ef6e81c 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -5,6 +5,7 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerLoadServerInfo(IDZHandlerBase): cmd_codes = [0x0006] * IDZConstants.NUM_VERS rsp_codes = [0x0007] * IDZConstants.NUM_VERS @@ -12,14 +13,14 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x04b0 - + self.size = 0x04B0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) offset = 0 - if self.version >= IDZConstants.VER_IDZ_210: + if self.version >= IDZConstants.VER_IDZ_210: offset = 2 - + news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" @@ -27,34 +28,69 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): len_news = len(news_str) len_error = len(err_str) - struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ba0 - + self.size = 0x0BA0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) + class IDZHandlerLoadTeamRankingB(IDZHandlerBase): - cmd_codes = [0x00bb, 0x00bb, 0x00a9, 0x00a9] - rsp_codes = [0x00a8] * 4 + cmd_codes = [0x00BB, 0x00BB, 0x00A9, 0x00A9] + rsp_codes = [0x00A8] * 4 name = "load_team_ranking_b" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x0ba0 - + self.size = 0x0BA0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/load_top_ten.py b/titles/idz/handlers/load_top_ten.py index ef2a158..09a9f5f 100644 --- a/titles/idz/handlers/load_top_ten.py +++ b/titles/idz/handlers/load_top_ten.py @@ -5,9 +5,10 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerLoadTopTen(IDZHandlerBase): - cmd_codes = [0x012c] * 4 - rsp_codes = [0x00ce] * 4 + cmd_codes = [0x012C] * 4 + rsp_codes = [0x00CE] * 4 name = "load_top_ten" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: @@ -19,12 +20,16 @@ class IDZHandlerLoadTopTen(IDZHandlerBase): tracks_dates: List[Tuple[int, int]] = [] for i in range(32): tracks_dates.append( - (struct.unpack_from(" None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0020 - + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) profile_data = { - "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, - "expire_time": int((datetime.now() + timedelta(hours=1)).timestamp() / 1000) + "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, + "expire_time": int( + (datetime.now() + timedelta(hours=1)).timestamp() / 1000 + ), } user_id = struct.unpack_from(" bytearray: struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/save_profile.py b/titles/idz/handlers/save_profile.py index 652dbd6..3f5311d 100644 --- a/titles/idz/handlers/save_profile.py +++ b/titles/idz/handlers/save_profile.py @@ -4,12 +4,13 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerSaveProfile(IDZHandlerBase): cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143] name = "save_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/save_time_attack.py b/titles/idz/handlers/save_time_attack.py index a28a598..bea83af 100644 --- a/titles/idz/handlers/save_time_attack.py +++ b/titles/idz/handlers/save_time_attack.py @@ -5,18 +5,19 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerSaveTimeAttack(IDZHandlerBase): cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136] - rsp_codes = [0x00ce, 0x00ce, 0x00cd, 0x00cd] + rsp_codes = [0x00CE, 0x00CE, 0x00CD, 0x00CD] name = "save_time_attack" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x00b0 + self.size = 0x00B0 if self.version > IDZConstants.VER_IDZ_130: - self.size = 0x00f0 - + self.size = 0x00F0 + def handle(self, data: bytes) -> bytearray: ret = super().handle(data) - return ret \ No newline at end of file + return ret diff --git a/titles/idz/handlers/save_topic.py b/titles/idz/handlers/save_topic.py index 9499385..090ce52 100644 --- a/titles/idz/handlers/save_topic.py +++ b/titles/idz/handlers/save_topic.py @@ -4,6 +4,7 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerSaveTopic(IDZHandlerBase): cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091] rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092] @@ -11,7 +12,7 @@ class IDZHandlerSaveTopic(IDZHandlerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x05d0 - + self.size = 0x05D0 + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/unknown.py b/titles/idz/handlers/unknown.py index ee3046a..8998d81 100644 --- a/titles/idz/handlers/unknown.py +++ b/titles/idz/handlers/unknown.py @@ -4,8 +4,9 @@ from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig + class IDZHandlerUnknown(IDZHandlerBase): - cmd_codes = [0x00ad, 0x00ad, 0x00a2, 0x00a2] + cmd_codes = [0x00AD, 0x00AD, 0x00A2, 0x00A2] name = "unknown" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: diff --git a/titles/idz/handlers/unlock_profile.py b/titles/idz/handlers/unlock_profile.py index a5c8310..1be50f5 100644 --- a/titles/idz/handlers/unlock_profile.py +++ b/titles/idz/handlers/unlock_profile.py @@ -5,16 +5,17 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerUnlockProfile(IDZHandlerBase): - cmd_codes = [0x006f, 0x006f, 0x006b, 0x006b] - rsp_codes = [0x0070, 0x0070, 0x006c, 0x006c] + cmd_codes = [0x006F, 0x006F, 0x006B, 0x006B] + rsp_codes = [0x0070, 0x0070, 0x006C, 0x006C] name = "unlock_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) self.size = 0x0010 - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) struct.pack_into(" None: super().__init__(core_cfg, game_cfg, version) - self.size = 0x02b0 - + self.size = 0x02B0 + def handle(self, data: bytes) -> bytearray: - return super().handle(data) - + return super().handle(data) + def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray: pass diff --git a/titles/idz/handlers/update_story_clear_num.py b/titles/idz/handlers/update_story_clear_num.py index 46f2689..bcf44a5 100644 --- a/titles/idz/handlers/update_story_clear_num.py +++ b/titles/idz/handlers/update_story_clear_num.py @@ -5,22 +5,23 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandlerUpdateStoryClearNum(IDZHandlerBase): - cmd_codes = [0x007f, 0x097f, 0x013d, 0x0144] - rsp_codes = [0x0080, 0x013e, 0x013e, 0x0145] + cmd_codes = [0x007F, 0x097F, 0x013D, 0x0144] + rsp_codes = [0x0080, 0x013E, 0x013E, 0x0145] name = "update_story_clear_num" - + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) if self.version == IDZConstants.VER_IDZ_110: self.size = 0x0220 elif self.version == IDZConstants.VER_IDZ_130: - self.size = 0x04f0 + self.size = 0x04F0 elif self.version == IDZConstants.VER_IDZ_210: self.size = 0x0510 elif self.version == IDZConstants.VER_IDZ_230: self.size = 0x0800 - + def handle(self, data: bytes) -> bytearray: return super().handle(data) diff --git a/titles/idz/handlers/update_team_points.py b/titles/idz/handlers/update_team_points.py index c1f69ca..a23d843 100644 --- a/titles/idz/handlers/update_team_points.py +++ b/titles/idz/handlers/update_team_points.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateTeamPoints(IDZHandlerBase): - cmd_codes = [0x0081, 0x0081, 0x007b, 0x007b] + cmd_codes = [0x0081, 0x0081, 0x007B, 0x007B] name = "unlock_profile" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/handlers/update_ui_report.py b/titles/idz/handlers/update_ui_report.py index b77b4ea..7e99b40 100644 --- a/titles/idz/handlers/update_ui_report.py +++ b/titles/idz/handlers/update_ui_report.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateUIReport(IDZHandlerBase): - cmd_codes = [0x0084, 0x0084, 0x007e, 0x007e] + cmd_codes = [0x0084, 0x0084, 0x007E, 0x007E] name = "update_ui_report" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/handlers/update_user_log.py b/titles/idz/handlers/update_user_log.py index 78679eb..c862f52 100644 --- a/titles/idz/handlers/update_user_log.py +++ b/titles/idz/handlers/update_user_log.py @@ -5,13 +5,14 @@ from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants + class IDZHandleUpdateUserLog(IDZHandlerBase): - cmd_codes = [0x00bd, 0x00bd, 0x00ab, 0x00b3] + cmd_codes = [0x00BD, 0x00BD, 0x00AB, 0x00B3] name = "update_user_log" def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: super().__init__(core_cfg, game_cfg, version) - + def handle(self, data: bytes) -> bytearray: - ret = super().handle(data) + ret = super().handle(data) return ret diff --git a/titles/idz/index.py b/titles/idz/index.py index 5b60e5a..0f26a30 100644 --- a/titles/idz/index.py +++ b/titles/idz/index.py @@ -16,6 +16,7 @@ from .userdb import IDZUserDBFactory, IDZUserDBWeb, IDZKey from .echo import IDZEcho from .handlers import IDZHandlerLoadConfigB + class IDZServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg @@ -51,13 +52,16 @@ class IDZServlet: level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) self.logger.inited = True - + @classmethod def rsaHashKeyN(cls, data): hash_ = 0 for i in data: - hash_ = hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) ^ IDZConstants.HASH_LUT[i & 0xf] - hash_ &= 0xffffffff + hash_ = ( + hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) + ^ IDZConstants.HASH_LUT[i & 0xF] + ) + hash_ &= 0xFFFFFFFF return hash_ @classmethod @@ -77,14 +81,18 @@ class IDZServlet: logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start") return (False, "", "") - hostname = core_cfg.title.hostname if not game_cfg.server.hostname else game_cfg.server.hostname + hostname = ( + core_cfg.title.hostname + if not game_cfg.server.hostname + else game_cfg.server.hostname + ) return ( True, f"", f"{hostname}:{game_cfg.ports.userdb}", ) - def setup(self): + def setup(self): for key in self.game_cfg.rsa_keys: if "N" not in key or "d" not in key or "e" not in key: self.logger.error(f"Invalid IDZ key {key}") @@ -92,14 +100,14 @@ class IDZServlet: hashN = self.rsaHashKeyN(str(key["N"]).encode()) self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN)) - + if len(self.rsa_keys) <= 0: self.logger.error("No valid RSA keys provided! IDZ cannot start!") return handler_map = [{} for _ in range(IDZConstants.NUM_VERS)] handler_mod = mod = importlib.import_module(f"titles.idz.handlers") - + for cls_name in dir(handler_mod): if cls_name.startswith("__"): continue @@ -109,7 +117,7 @@ class IDZServlet: mod_cmds: List = getattr(mod, "cmd_codes") while len(mod_cmds) < IDZConstants.NUM_VERS: mod_cmds.append(None) - + for i in range(len(mod_cmds)): if mod_cmds[i] is None: mod_cmds[i] = mod_cmds[i - 1] @@ -119,27 +127,47 @@ class IDZServlet: except AttributeError as e: continue - endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}")\ - .listen(IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map)) - - reactor.listenUDP(self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg)) - reactor.listenUDP(self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg)) - + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}", + ).listen( + IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map) + ) + + reactor.listenUDP( + self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - req_raw = request.content.getvalue() + req_raw = request.content.getvalue() self.logger.info(f"IDZ POST request: {url_path} - {req_raw}") return b"" def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - self.logger.info(f"IDZ GET request: {url_path}") - request.responseHeaders.setRawHeaders('Content-Type', [b"text/plain; charset=utf-8"]) - request.responseHeaders.setRawHeaders("Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"]) - - news = self.game_cfg.server.news if self.game_cfg.server.news else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + self.logger.info(f"IDZ GET request: {url_path}") + request.responseHeaders.setRawHeaders( + "Content-Type", [b"text/plain; charset=utf-8"] + ) + request.responseHeaders.setRawHeaders( + "Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"] + ) + + news = ( + self.game_cfg.server.news + if self.game_cfg.server.news + else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + ) news += "\r\n" news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index 95a2eb9..2f70ba4 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -19,6 +19,7 @@ from .handlers import IDZHandlerBase HANDLER_MAP: List[Dict] + class IDZKey: def __init__(self, n, d, e, hashN: int) -> None: self.N = n @@ -26,9 +27,16 @@ class IDZKey: self.e = e self.hashN = hashN + class IDZUserDBProtocol(Protocol): - def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, keys: List[IDZKey], handlers: List[Dict]) -> None: - self.logger = logging.getLogger('idz') + def __init__( + self, + core_cfg: CoreConfig, + game_cfg: IDZConfig, + keys: List[IDZKey], + handlers: List[Dict], + ) -> None: + self.logger = logging.getLogger("idz") self.core_config = core_cfg self.game_config = game_cfg self.rsa_keys = keys @@ -37,14 +45,14 @@ class IDZUserDBProtocol(Protocol): self.version = None self.version_internal = None self.skip_next = False - + def append_padding(self, data: bytes): """Appends 0s to the end of the data until it's at the correct size""" length = struct.unpack_from(" None: self.logger.debug(f"{self.transport.getPeer().host} Connected") base = 0 @@ -57,13 +65,17 @@ class IDZUserDBProtocol(Protocol): rsa_key = random.choice(self.rsa_keys) key_enc: int = pow(base, rsa_key.e, rsa_key.N) - result = key_enc.to_bytes(0x40, "little") + struct.pack(" None: + def connectionLost(self, reason) -> None: self.logger.debug( f"{self.transport.getPeer().host} Disconnected - {reason.value}" ) @@ -84,7 +96,7 @@ class IDZUserDBProtocol(Protocol): self.transport.write(b"\x00") return - elif magic == 0x01020304: + elif magic == 0x01020304: self.version = int(data_dec[16:19].decode()) if self.version == 110: @@ -99,10 +111,12 @@ class IDZUserDBProtocol(Protocol): self.logger.warn(f"Bad version v{self.version}") self.version = None self.version_internal = None - - self.logger.debug(f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}") + + self.logger.debug( + f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}" + ) return - + elif self.skip_next: self.skip_next = False self.transport.write(b"\x00") @@ -110,19 +124,25 @@ class IDZUserDBProtocol(Protocol): elif self.version is None: # We didn't get a handshake before, and this isn't one now, so we're up the creek - self.logger.info(f"Bad UserDB request from from {self.transport.getPeer().host}") + self.logger.info( + f"Bad UserDB request from from {self.transport.getPeer().host}" + ) self.transport.write(b"\x00") return cmd = struct.unpack_from(" None: + def __init__( + self, + cfg: CoreConfig, + game_cfg: IDZConfig, + keys: List[IDZKey], + handlers: List[Dict], + ) -> None: self.core_config = cfg self.game_config = game_cfg self.keys = keys self.handlers = handlers def buildProtocol(self, addr): - return IDZUserDBProtocol(self.core_config, self.game_config, self.keys, self.handlers) + return IDZUserDBProtocol( + self.core_config, self.game_config, self.keys, self.handlers + ) + class IDZUserDBWeb(resource.Resource): def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): @@ -151,12 +180,16 @@ class IDZUserDBWeb(resource.Resource): self.isLeaf = True self.core_config = core_cfg self.game_config = game_cfg - self.logger = logging.getLogger('idz') - + self.logger = logging.getLogger("idz") + def render_POST(self, request: Request) -> bytes: - self.logger.info(f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}") + self.logger.info( + f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}" + ) return b"" def render_GET(self, request: Request) -> bytes: - self.logger.info(f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}") - return b"" \ No newline at end of file + self.logger.info( + f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}" + ) + return b"" diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 9f5c8af..171378c 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -300,7 +300,9 @@ class Mai2Base: ): for fsr in upsert["userFriendSeasonRankingList"]: fsr["recordDate"] = ( - datetime.strptime(fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"), + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), ) self.data.item.put_friend_season_ranking(user_id, fsr) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 9694aba..dcc7e29 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -39,7 +39,7 @@ class Mai2Constants: "maimai DX Splash PLUS", "maimai DX Universe", "maimai DX Universe PLUS", - "maimai DX Festival" + "maimai DX Festival", ) @classmethod diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 3618f4d..1b92842 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -38,7 +38,7 @@ class Mai2Servlet: Mai2SplashPlus, Mai2Universe, Mai2UniversePlus, - Mai2Festival + Mai2Festival, ] self.logger = logging.getLogger("mai2") @@ -134,9 +134,7 @@ class Mai2Servlet: req_data = json.loads(unzip) - self.logger.info( - f"v{version} {endpoint} request from {client_ip}" - ) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 980623e..6280bbb 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -402,7 +402,7 @@ class Mai2ItemData(BaseData): if result is None: self.logger.warn( f"put_friend_season_ranking: failed to insert", - f"friend_season_ranking! aime_id: {aime_id}" + f"friend_season_ranking! aime_id: {aime_id}", ) return None return result.lastrowid diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index b3802e5..3cb42d1 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -299,9 +299,11 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: - sql = select(detail).where( - and_(detail.c.user == user_id, detail.c.version <= version) - ).order_by(detail.c.version.desc()) + sql = ( + select(detail) + .where(and_(detail.c.user == user_id, detail.c.version <= version)) + .order_by(detail.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -324,9 +326,11 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: - sql = select(ghost).where( - and_(ghost.c.user == user_id, ghost.c.version_int <= version) - ).order_by(ghost.c.version.desc()) + sql = ( + select(ghost) + .where(and_(ghost.c.user == user_id, ghost.c.version_int <= version)) + .order_by(ghost.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -349,9 +353,11 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: - sql = select(extend).where( - and_(extend.c.user == user_id, extend.c.version <= version) - ).order_by(extend.c.version.desc()) + sql = ( + select(extend) + .where(and_(extend.c.user == user_id, extend.c.version <= version)) + .order_by(extend.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -374,9 +380,11 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: - sql = select(option).where( - and_(option.c.user == user_id, option.c.version <= version) - ).order_by(option.c.version.desc()) + sql = ( + select(option) + .where(and_(option.c.user == user_id, option.c.version <= version)) + .order_by(option.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -399,9 +407,11 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: - sql = select(rating).where( - and_(rating.c.user == user_id, rating.c.version <= version) - ).order_by(rating.c.version.desc()) + sql = ( + select(rating) + .where(and_(rating.c.user == user_id, rating.c.version <= version)) + .order_by(rating.c.version.desc()) + ) result = self.execute(sql) if result is None: diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 10bb1a8..4f7619c 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -452,8 +452,7 @@ class OngekiBase: tmp.pop("id") items.append(tmp) - xout = kind * 10000000000 + \ - (data["nextIndex"] % 10000000000) + len(items) + xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items) if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0 @@ -852,8 +851,7 @@ class OngekiBase: ) if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_options( - user_id, upsert["userOption"][0]) + self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 23eeb6c..06155a1 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -97,7 +97,7 @@ class OngekiBright(OngekiBase): "userId": data["userId"], "length": 0, "nextIndex": 0, - "userCharacterList": [] + "userCharacterList": [], } max_ct = data["maxCount"] @@ -548,7 +548,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintPlaylogApi" + "apiName": "CMUpsertUserPrintPlaylogApi", } def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: @@ -556,7 +556,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintlogApi" + "apiName": "CMUpsertUserPrintlogApi", } def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index d826fba..27d90f8 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -705,9 +705,7 @@ class OngekiItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 0e87397..40e6444 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -74,27 +74,21 @@ class PokkenBase: return res.SerializeToString() - def handle_save_client_log( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG return res.SerializeToString() - def handle_check_diagnosis( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS return res.SerializeToString() - def handle_load_client_settings( - self, request: jackal_pb2.Request - ) -> bytes: + def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS @@ -129,26 +123,28 @@ class PokkenBase: ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) return res.SerializeToString() - + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_USER - access_code = request.load_user.access_code + access_code = request.load_user.access_code load_usr = jackal_pb2.LoadUserResponseData() user_id = self.data.card.get_user_id_from_card(access_code) if user_id is None and self.game_cfg.server.auto_register: user_id = self.data.user.create_user() card_id = self.data.card.create_card(user_id, access_code) - - self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") - + + self.logger.info( + f"Register new card {access_code} (UserId {user_id}, CardId {card_id})" + ) + elif user_id is None: self.logger.info(f"Registration of card {access_code} blocked!") res.load_user.CopyFrom(load_usr) return res.SerializeToString() - + """ TODO: Add repeated values tutorial_progress_flag @@ -168,12 +164,12 @@ class PokkenBase: load_usr.load_hash = 1 load_usr.cardlock_status = False load_usr.banapass_id = user_id - load_usr.access_code = access_code + load_usr.access_code = access_code load_usr.precedent_release_flag = 0xFFFFFFFF - + if profile is None: - profile_id = self.data.profile.create_profile(user_id) - profile_dict = {'id': profile_id, 'user': user_id} + profile_id = self.data.profile.create_profile(user_id) + profile_dict = {"id": profile_id, "user": user_id} pokemon_data = [] tutorial_progress = [] rankmatch_progress = [] @@ -181,10 +177,12 @@ class PokkenBase: event_achievement_flag = [] event_achievement_param = [] load_usr.new_card_flag = True - + else: - profile_dict = { k: v for k, v in profile._asdict().items() if v is not None } - self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})") + profile_dict = {k: v for k, v in profile._asdict().items() if v is not None} + self.logger.info( + f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})" + ) pokemon_data = self.data.profile.get_all_pokemon_data(user_id) tutorial_progress = [] rankmatch_progress = [] @@ -193,76 +191,78 @@ class PokkenBase: event_achievement_param = [] load_usr.new_card_flag = False - load_usr.navi_newbie_flag = profile_dict.get('navi_newbie_flag', True) - load_usr.navi_enable_flag = profile_dict.get('navi_enable_flag', True) - load_usr.pad_vibrate_flag = profile_dict.get('pad_vibrate_flag', True) - load_usr.home_region_code = profile_dict.get('home_region_code', 0) - load_usr.home_loc_name = profile_dict.get('home_loc_name', "") - load_usr.pref_code = profile_dict.get('pref_code', 0) - load_usr.trainer_name = profile_dict.get('trainer_name', "Newb" + str(random.randint(1111,999999))) - load_usr.trainer_rank_point = profile_dict.get('trainer_rank_point', 0) - load_usr.wallet = profile_dict.get('wallet', 0) - load_usr.fight_money = profile_dict.get('fight_money', 0) - load_usr.score_point = profile_dict.get('score_point', 0) - load_usr.grade_max_num = profile_dict.get('grade_max_num', 0) - load_usr.extra_counter = profile_dict.get('extra_counter', 0) - load_usr.total_play_days = profile_dict.get('total_play_days', 0) - load_usr.play_date_time = profile_dict.get('play_date_time', 0) - load_usr.lucky_box_fail_num = profile_dict.get('lucky_box_fail_num', 0) - load_usr.event_reward_get_flag = profile_dict.get('event_reward_get_flag', 0) - load_usr.rank_pvp_all = profile_dict.get('rank_pvp_all', 0) - load_usr.rank_pvp_loc = profile_dict.get('rank_pvp_loc', 0) - load_usr.rank_cpu_all = profile_dict.get('rank_cpu_all', 0) - load_usr.rank_cpu_loc = profile_dict.get('rank_cpu_loc', 0) - load_usr.rank_event = profile_dict.get('rank_event', 0) - load_usr.awake_num = profile_dict.get('awake_num', 0) - load_usr.use_support_num = profile_dict.get('use_support_num', 0) - load_usr.rankmatch_flag = profile_dict.get('rankmatch_flag', 0) - load_usr.rankmatch_max = profile_dict.get('rankmatch_max', 0) - load_usr.rankmatch_success = profile_dict.get('rankmatch_success', 0) - load_usr.beat_num = profile_dict.get('beat_num', 0) - load_usr.title_text_id = profile_dict.get('title_text_id', 0) - load_usr.title_plate_id = profile_dict.get('title_plate_id', 0) - load_usr.title_decoration_id = profile_dict.get('title_decoration_id', 0) - load_usr.navi_trainer = profile_dict.get('navi_trainer', 0) - load_usr.navi_version_id = profile_dict.get('navi_version_id', 0) - load_usr.aid_skill = profile_dict.get('aid_skill', 0) - load_usr.comment_text_id = profile_dict.get('comment_text_id', 0) - load_usr.comment_word_id = profile_dict.get('comment_word_id', 0) - load_usr.latest_use_pokemon = profile_dict.get('latest_use_pokemon', 0) - load_usr.ex_ko_num = profile_dict.get('ex_ko_num', 0) - load_usr.wko_num = profile_dict.get('wko_num', 0) - load_usr.timeup_win_num = profile_dict.get('timeup_win_num', 0) - load_usr.cool_ko_num = profile_dict.get('cool_ko_num', 0) - load_usr.perfect_ko_num = profile_dict.get('perfect_ko_num', 0) - load_usr.record_flag = profile_dict.get('record_flag', 0) - load_usr.site_register_status = profile_dict.get('site_register_status', 0) - load_usr.continue_num = profile_dict.get('continue_num', 0) + load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True) + load_usr.navi_enable_flag = profile_dict.get("navi_enable_flag", True) + load_usr.pad_vibrate_flag = profile_dict.get("pad_vibrate_flag", True) + load_usr.home_region_code = profile_dict.get("home_region_code", 0) + load_usr.home_loc_name = profile_dict.get("home_loc_name", "") + load_usr.pref_code = profile_dict.get("pref_code", 0) + load_usr.trainer_name = profile_dict.get( + "trainer_name", "Newb" + str(random.randint(1111, 999999)) + ) + load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) + load_usr.wallet = profile_dict.get("wallet", 0) + load_usr.fight_money = profile_dict.get("fight_money", 0) + load_usr.score_point = profile_dict.get("score_point", 0) + load_usr.grade_max_num = profile_dict.get("grade_max_num", 0) + load_usr.extra_counter = profile_dict.get("extra_counter", 0) + load_usr.total_play_days = profile_dict.get("total_play_days", 0) + load_usr.play_date_time = profile_dict.get("play_date_time", 0) + load_usr.lucky_box_fail_num = profile_dict.get("lucky_box_fail_num", 0) + load_usr.event_reward_get_flag = profile_dict.get("event_reward_get_flag", 0) + load_usr.rank_pvp_all = profile_dict.get("rank_pvp_all", 0) + load_usr.rank_pvp_loc = profile_dict.get("rank_pvp_loc", 0) + load_usr.rank_cpu_all = profile_dict.get("rank_cpu_all", 0) + load_usr.rank_cpu_loc = profile_dict.get("rank_cpu_loc", 0) + load_usr.rank_event = profile_dict.get("rank_event", 0) + load_usr.awake_num = profile_dict.get("awake_num", 0) + load_usr.use_support_num = profile_dict.get("use_support_num", 0) + load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) + load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0) + load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0) + load_usr.beat_num = profile_dict.get("beat_num", 0) + load_usr.title_text_id = profile_dict.get("title_text_id", 0) + load_usr.title_plate_id = profile_dict.get("title_plate_id", 0) + load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0) + load_usr.navi_trainer = profile_dict.get("navi_trainer", 0) + load_usr.navi_version_id = profile_dict.get("navi_version_id", 0) + load_usr.aid_skill = profile_dict.get("aid_skill", 0) + load_usr.comment_text_id = profile_dict.get("comment_text_id", 0) + load_usr.comment_word_id = profile_dict.get("comment_word_id", 0) + load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0) + load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0) + load_usr.wko_num = profile_dict.get("wko_num", 0) + load_usr.timeup_win_num = profile_dict.get("timeup_win_num", 0) + load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0) + load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0) + load_usr.record_flag = profile_dict.get("record_flag", 0) + load_usr.site_register_status = profile_dict.get("site_register_status", 0) + load_usr.continue_num = profile_dict.get("continue_num", 0) - load_usr.avatar_body = profile_dict.get('avatar_body', 0) - load_usr.avatar_gender = profile_dict.get('avatar_gender', 0) - load_usr.avatar_background = profile_dict.get('avatar_background', 0) - load_usr.avatar_head = profile_dict.get('avatar_head', 0) - load_usr.avatar_battleglass = profile_dict.get('avatar_battleglass', 0) - load_usr.avatar_face0 = profile_dict.get('avatar_face0', 0) - load_usr.avatar_face1 = profile_dict.get('avatar_face1', 0) - load_usr.avatar_face2 = profile_dict.get('avatar_face2', 0) - load_usr.avatar_bodyall = profile_dict.get('avatar_bodyall', 0) - load_usr.avatar_wear = profile_dict.get('avatar_wear', 0) - load_usr.avatar_accessory = profile_dict.get('avatar_accessory', 0) - load_usr.avatar_stamp = profile_dict.get('avatar_stamp', 0) + load_usr.avatar_body = profile_dict.get("avatar_body", 0) + load_usr.avatar_gender = profile_dict.get("avatar_gender", 0) + load_usr.avatar_background = profile_dict.get("avatar_background", 0) + load_usr.avatar_head = profile_dict.get("avatar_head", 0) + load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0) + load_usr.avatar_face0 = profile_dict.get("avatar_face0", 0) + load_usr.avatar_face1 = profile_dict.get("avatar_face1", 0) + load_usr.avatar_face2 = profile_dict.get("avatar_face2", 0) + load_usr.avatar_bodyall = profile_dict.get("avatar_bodyall", 0) + load_usr.avatar_wear = profile_dict.get("avatar_wear", 0) + load_usr.avatar_accessory = profile_dict.get("avatar_accessory", 0) + load_usr.avatar_stamp = profile_dict.get("avatar_stamp", 0) - load_usr.event_state = profile_dict.get('event_state', 0) - load_usr.event_id = profile_dict.get('event_id', 0) - load_usr.sp_bonus_category_id_1 = profile_dict.get('sp_bonus_category_id_1', 0) - load_usr.sp_bonus_key_value_1 = profile_dict.get('sp_bonus_key_value_1', 0) - load_usr.sp_bonus_category_id_2 = profile_dict.get('sp_bonus_category_id_2', 0) - load_usr.sp_bonus_key_value_2 = profile_dict.get('sp_bonus_key_value_2', 0) - load_usr.last_play_event_id = profile_dict.get('last_play_event_id', 0) + load_usr.event_state = profile_dict.get("event_state", 0) + load_usr.event_id = profile_dict.get("event_id", 0) + load_usr.sp_bonus_category_id_1 = profile_dict.get("sp_bonus_category_id_1", 0) + load_usr.sp_bonus_key_value_1 = profile_dict.get("sp_bonus_key_value_1", 0) + load_usr.sp_bonus_category_id_2 = profile_dict.get("sp_bonus_category_id_2", 0) + load_usr.sp_bonus_key_value_2 = profile_dict.get("sp_bonus_key_value_2", 0) + load_usr.last_play_event_id = profile_dict.get("last_play_event_id", 0) res.load_user.CopyFrom(load_usr) return res.SerializeToString() - + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -288,22 +288,30 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SAVE_CHARGE return res.SerializeToString() - def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: - return {} - - def handle_matching_start_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + def handle_matching_noop( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: return {} - def handle_matching_is_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + def handle_matching_start_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} + + def handle_matching_is_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: """ "sessionId":"12345678", "A":{ "pcb_id": data["data"]["must"]["pcb_id"], "gip": client_ip - }, + }, "list":[] """ return {} - - def handle_matching_stop_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: - return {} \ No newline at end of file + + def handle_matching_stop_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} diff --git a/titles/pokken/config.py b/titles/pokken/config.py index e25a8c7..84da8d2 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -59,6 +59,7 @@ class PokkenServerConfig: self.__config, "pokken", "server", "auto_register", default=True ) + class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 81b2ebc..2eb5357 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,5 +1,6 @@ from enum import Enum + class PokkenConstants: GAME_CODE = "SDAK" diff --git a/titles/pokken/database.py b/titles/pokken/database.py index c940d83..272cfd8 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -3,6 +3,7 @@ from core.config import CoreConfig from .schema import * + class PokkenData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index adfde88..bccdcaf 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -114,19 +114,19 @@ class PokkenServlet(resource.Resource): endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ pokken_request.type ].name.lower() - + self.logger.debug(pokken_request) handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: self.logger.warn(f"No handler found for message type {endpoint}") return self.base.handle_noop(pokken_request) - + self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") - + ret = handler(pokken_request) return ret - + def handle_matching(self, request: Request) -> bytes: content = request.content.getvalue() client_ip = Utils.get_ip_addr(request) @@ -135,26 +135,37 @@ class PokkenServlet(resource.Resource): self.logger.info("Empty matching request") return json.dumps(self.base.handle_matching_noop()).encode() - json_content = ast.literal_eval(content.decode().replace('null', 'None').replace('true', 'True').replace('false', 'False')) + json_content = ast.literal_eval( + content.decode() + .replace("null", "None") + .replace("true", "True") + .replace("false", "False") + ) self.logger.info(f"Matching {json_content['call']} request") self.logger.debug(json_content) - handler = getattr(self.base, f"handle_matching_{inflection.underscore(json_content['call'])}", None) + handler = getattr( + self.base, + f"handle_matching_{inflection.underscore(json_content['call'])}", + None, + ) if handler is None: - self.logger.warn(f"No handler found for message type {json_content['call']}") + self.logger.warn( + f"No handler found for message type {json_content['call']}" + ) return json.dumps(self.base.handle_matching_noop()).encode() - + ret = handler(json_content, client_ip) - + if ret is None: - ret = {} + ret = {} if "result" not in ret: ret["result"] = "true" if "data" not in ret: ret["data"] = {} if "timestamp" not in ret: ret["timestamp"] = int(datetime.now().timestamp() * 1000) - + self.logger.debug(f"Response {ret}") return json.dumps(ret).encode() diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py index 686b32f..4919ea0 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -9,19 +9,26 @@ from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata item = Table( - 'pokken_item', + "pokken_item", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), - Column('category', Integer), - Column('content', Integer), - Column('type', Integer), - UniqueConstraint('user', 'category', 'content', 'type', name='pokken_item_uk'), + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("category", Integer), + Column("content", Integer), + Column("type", Integer), + UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"), mysql_charset="utf8mb4", ) + class PokkenItemData(BaseData): """ Items obtained as rewards """ + pass diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py index aec6bd3..c84ec63 100644 --- a/titles/pokken/schema/match.py +++ b/titles/pokken/schema/match.py @@ -10,29 +10,35 @@ from core.data.schema import BaseData, metadata # Pokken sends depressingly little match data... match_data = Table( - 'pokken_match_data', + "pokken_match_data", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('num_games', Integer), - Column('play_modes', JSON), - Column('results', JSON), - Column('ex_ko_num', Integer), - Column('wko_num', Integer), - Column('timeup_win_num', Integer), - Column('cool_ko_num', Integer), - Column('perfect_ko_num', Integer), - Column('use_navi', Integer), - Column('use_navi_cloth', Integer), - Column('use_aid_skill', Integer), - Column('play_date', TIMESTAMP), + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("num_games", Integer), + Column("play_modes", JSON), + Column("results", JSON), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("use_navi", Integer), + Column("use_navi_cloth", Integer), + Column("use_aid_skill", Integer), + Column("play_date", TIMESTAMP), mysql_charset="utf8mb4", ) + class PokkenMatchData(BaseData): """ Match logs """ + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: pass @@ -43,4 +49,4 @@ class PokkenMatchData(BaseData): pass def get_matches(self, limit: int = 20) -> Optional[List[Row]]: - pass \ No newline at end of file + pass diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 871ff9d..8e536f1 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -12,150 +12,179 @@ from ..const import PokkenConstants # Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me # Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue profile = Table( - 'pokken_profile', + "pokken_profile", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), - Column('trainer_name', String(16)), # optional - Column('home_region_code', Integer), - Column('home_loc_name', String(255)), - Column('pref_code', Integer), - Column('navi_newbie_flag', Boolean), - Column('navi_enable_flag', Boolean), - Column('pad_vibrate_flag', Boolean), - Column('trainer_rank_point', Integer), - Column('wallet', Integer), - Column('fight_money', Integer), - Column('score_point', Integer), - Column('grade_max_num', Integer), - Column('extra_counter', Integer), # Optional - Column('tutorial_progress_flag', JSON), # Repeated, Integer - Column('total_play_days', Integer), - Column('play_date_time', Integer), - Column('achievement_flag', JSON), # Repeated, Integer - Column('lucky_box_fail_num', Integer), - Column('event_reward_get_flag', Integer), - Column('rank_pvp_all', Integer), - Column('rank_pvp_loc', Integer), - Column('rank_cpu_all', Integer), - Column('rank_cpu_loc', Integer), - Column('rank_event', Integer), - Column('awake_num', Integer), - Column('use_support_num', Integer), - Column('rankmatch_flag', Integer), - Column('rankmatch_max', Integer), # Optional - Column('rankmatch_progress', JSON), # Repeated, Integer - Column('rankmatch_success', Integer), # Optional - Column('beat_num', Integer), # Optional - Column('title_text_id', Integer), - Column('title_plate_id', Integer), - Column('title_decoration_id', Integer), - Column('support_pokemon_list', JSON), # Repeated, Integer - Column('support_set_1_1', Integer), # Repeated, Integer - Column('support_set_1_2', Integer), - Column('support_set_2_1', Integer), # Repeated, Integer - Column('support_set_2_2', Integer), - Column('support_set_3_1', Integer), # Repeated, Integer - Column('support_set_3_2', Integer), - Column('navi_trainer', Integer), - Column('navi_version_id', Integer), - Column('aid_skill_list', JSON), # Repeated, Integer - Column('aid_skill', Integer), - Column('comment_text_id', Integer), - Column('comment_word_id', Integer), - Column('latest_use_pokemon', Integer), - Column('ex_ko_num', Integer), - Column('wko_num', Integer), - Column('timeup_win_num', Integer), - Column('cool_ko_num', Integer), - Column('perfect_ko_num', Integer), - Column('record_flag', Integer), - Column('continue_num', Integer), - Column('avatar_body', Integer), # Optional - Column('avatar_gender', Integer), # Optional - Column('avatar_background', Integer), # Optional - Column('avatar_head', Integer), # Optional - Column('avatar_battleglass', Integer), # Optional - Column('avatar_face0', Integer), # Optional - Column('avatar_face1', Integer), # Optional - Column('avatar_face2', Integer), # Optional - Column('avatar_bodyall', Integer), # Optional - Column('avatar_wear', Integer), # Optional - Column('avatar_accessory', Integer), # Optional - Column('avatar_stamp', Integer), # Optional - Column('event_state', Integer), - Column('event_id', Integer), - Column('sp_bonus_category_id_1', Integer), - Column('sp_bonus_key_value_1', Integer), - Column('sp_bonus_category_id_2', Integer), - Column('sp_bonus_key_value_2', Integer), - Column('last_play_event_id', Integer), # Optional - Column('event_achievement_flag', JSON), # Repeated, Integer - Column('event_achievement_param', JSON), # Repeated, Integer - Column('battle_num_vs_wan', Integer), # 4? - Column('win_vs_wan', Integer), - Column('battle_num_vs_lan', Integer), # 3? - Column('win_vs_lan', Integer), - Column('battle_num_vs_cpu', Integer), # 2 - Column('win_cpu', Integer), - Column('battle_num_tutorial', Integer), # 1? - mysql_charset="utf8mb4" + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("trainer_name", String(16)), # optional + Column("home_region_code", Integer), + Column("home_loc_name", String(255)), + Column("pref_code", Integer), + Column("navi_newbie_flag", Boolean), + Column("navi_enable_flag", Boolean), + Column("pad_vibrate_flag", Boolean), + Column("trainer_rank_point", Integer), + Column("wallet", Integer), + Column("fight_money", Integer), + Column("score_point", Integer), + Column("grade_max_num", Integer), + Column("extra_counter", Integer), # Optional + Column("tutorial_progress_flag", JSON), # Repeated, Integer + Column("total_play_days", Integer), + Column("play_date_time", Integer), + Column("achievement_flag", JSON), # Repeated, Integer + Column("lucky_box_fail_num", Integer), + Column("event_reward_get_flag", Integer), + Column("rank_pvp_all", Integer), + Column("rank_pvp_loc", Integer), + Column("rank_cpu_all", Integer), + Column("rank_cpu_loc", Integer), + Column("rank_event", Integer), + Column("awake_num", Integer), + Column("use_support_num", Integer), + Column("rankmatch_flag", Integer), + Column("rankmatch_max", Integer), # Optional + Column("rankmatch_progress", JSON), # Repeated, Integer + Column("rankmatch_success", Integer), # Optional + Column("beat_num", Integer), # Optional + Column("title_text_id", Integer), + Column("title_plate_id", Integer), + Column("title_decoration_id", Integer), + Column("support_pokemon_list", JSON), # Repeated, Integer + Column("support_set_1_1", Integer), # Repeated, Integer + Column("support_set_1_2", Integer), + Column("support_set_2_1", Integer), # Repeated, Integer + Column("support_set_2_2", Integer), + Column("support_set_3_1", Integer), # Repeated, Integer + Column("support_set_3_2", Integer), + Column("navi_trainer", Integer), + Column("navi_version_id", Integer), + Column("aid_skill_list", JSON), # Repeated, Integer + Column("aid_skill", Integer), + Column("comment_text_id", Integer), + Column("comment_word_id", Integer), + Column("latest_use_pokemon", Integer), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("record_flag", Integer), + Column("continue_num", Integer), + Column("avatar_body", Integer), # Optional + Column("avatar_gender", Integer), # Optional + Column("avatar_background", Integer), # Optional + Column("avatar_head", Integer), # Optional + Column("avatar_battleglass", Integer), # Optional + Column("avatar_face0", Integer), # Optional + Column("avatar_face1", Integer), # Optional + Column("avatar_face2", Integer), # Optional + Column("avatar_bodyall", Integer), # Optional + Column("avatar_wear", Integer), # Optional + Column("avatar_accessory", Integer), # Optional + Column("avatar_stamp", Integer), # Optional + Column("event_state", Integer), + Column("event_id", Integer), + Column("sp_bonus_category_id_1", Integer), + Column("sp_bonus_key_value_1", Integer), + Column("sp_bonus_category_id_2", Integer), + Column("sp_bonus_key_value_2", Integer), + Column("last_play_event_id", Integer), # Optional + Column("event_achievement_flag", JSON), # Repeated, Integer + Column("event_achievement_param", JSON), # Repeated, Integer + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_num_tutorial", Integer), # 1? + mysql_charset="utf8mb4", ) pokemon_data = Table( - 'pokken_pokemon_data', + "pokken_pokemon_data", metadata, - Column('id', Integer, primary_key=True, nullable=False), - Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('char_id', Integer, nullable=False), - Column('illustration_book_no', Integer), - Column('pokemon_exp', Integer), - Column('battle_num_vs_wan', Integer), # 4? - Column('win_vs_wan', Integer), - Column('battle_num_vs_lan', Integer), # 3? - Column('win_vs_lan', Integer), - Column('battle_num_vs_cpu', Integer), # 2 - Column('win_cpu', Integer), - Column('battle_all_num_tutorial', Integer), - Column('battle_num_tutorial', Integer), # 1? - Column('bp_point_atk', Integer), - Column('bp_point_res', Integer), - Column('bp_point_def', Integer), - Column('bp_point_sp', Integer), - UniqueConstraint('user', 'char_id', name="pokken_pokemon_data_uk"), - mysql_charset="utf8mb4" + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("char_id", Integer, nullable=False), + Column("illustration_book_no", Integer), + Column("pokemon_exp", Integer), + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_all_num_tutorial", Integer), + Column("battle_num_tutorial", Integer), # 1? + Column("bp_point_atk", Integer), + Column("bp_point_res", Integer), + Column("bp_point_def", Integer), + Column("bp_point_sp", Integer), + UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"), + mysql_charset="utf8mb4", ) + class PokkenProfileData(BaseData): def create_profile(self, user_id: int) -> Optional[int]: - sql = insert(profile).values(user = user_id) - conflict = sql.on_duplicate_key_update(user = user_id) - + sql = insert(profile).values(user=user_id) + conflict = sql.on_duplicate_key_update(user=user_id) + result = self.execute(conflict) if result is None: self.logger.error(f"Failed to create pokken profile for user {user_id}!") return None return result.lastrowid - + def set_profile_name(self, user_id: int, new_name: str) -> None: - sql = update(profile).where(profile.c.user == user_id).values(trainer_name = new_name) + sql = ( + update(profile) + .where(profile.c.user == user_id) + .values(trainer_name=new_name) + ) result = self.execute(sql) if result is None: - self.logger.error(f"Failed to update pokken profile name for user {user_id}!") - + self.logger.error( + f"Failed to update pokken profile name for user {user_id}!" + ) + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: pass - def add_profile_points(self, user_id: int, rank_pts: int, money: int, score_pts: int) -> None: + def add_profile_points( + self, user_id: int, rank_pts: int, money: int, score_pts: int + ) -> None: pass def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_pokemon_data(self, user_id: int, pokemon_id: int, illust_no: int, get_exp: int, atk: int, res: int, defe: int, sp: int) -> Optional[int]: + def put_pokemon_data( + self, + user_id: int, + pokemon_id: int, + illust_no: int, + get_exp: int, + atk: int, + res: int, + defe: int, + sp: int, + ) -> Optional[int]: pass def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: @@ -164,13 +193,24 @@ class PokkenProfileData(BaseData): def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: pass - def put_results(self, user_id: int, pokemon_id: int, match_type: int, match_result: int) -> None: + def put_results( + self, user_id: int, pokemon_id: int, match_type: int, match_result: int + ) -> None: """ Records the match stats (type and win/loss) for the pokemon and profile """ pass - def put_stats(self, user_id: int, exkos: int, wkos: int, timeout_wins: int, cool_kos: int, perfects: int, continues: int) -> None: + def put_stats( + self, + user_id: int, + exkos: int, + wkos: int, + timeout_wins: int, + cool_kos: int, + perfects: int, + continues: int, + ) -> None: """ Records profile stats """ diff --git a/titles/pokken/schema/static.py b/titles/pokken/schema/static.py index 63aa2dd..121ebc4 100644 --- a/titles/pokken/schema/static.py +++ b/titles/pokken/schema/static.py @@ -8,5 +8,6 @@ from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata + class PokkenStaticData(BaseData): - pass \ No newline at end of file + pass diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 35a6efc..ada40c6 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -200,18 +200,24 @@ class WaccaBase: self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + midnight_today_ts = int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today if last_login_time < midnight_today_ts: resp.firstLoginDaily = True - - # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak if midnight_today_ts - last_login_time > 86400: is_consec_day = False - self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) - + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) + if resp.firstLoginDaily: # TODO: Daily bonus pass @@ -624,17 +630,25 @@ class WaccaBase: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp: + if ( + item.itemType == WaccaConstants.ITEM_TYPES["wp"] + and not self.game_config.mods.infinite_wp + ): if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() - elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets: + elif ( + item.itemType == WaccaConstants.ITEM_TYPES["ticket"] + and not self.game_config.mods.infinite_tickets + ): for x in range(len(new_tickets)): if new_tickets[x][1] == item.itemId: - self.logger.debug(f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}") + self.logger.debug( + f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}" + ) self.data.item.spend_ticket(new_tickets[x][0]) new_tickets.pop(x) break @@ -865,7 +879,10 @@ class WaccaBase: user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp: + if ( + req.purchaseType == PurchaseType.PurchaseTypeWP + and not self.game_config.mods.infinite_wp + ): resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) @@ -1055,11 +1072,18 @@ class WaccaBase: ): if item.quantity > WaccaConstants.Difficulty.HARD.value: old_score = self.data.score.get_best_score( - user_id, item.itemId, item.quantity - ) + user_id, item.itemId, item.quantity + ) if not old_score: self.data.score.put_best_score( - user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0 + user_id, + item.itemId, + item.quantity, + 0, + [0] * 5, + [0] * 13, + 0, + 0, ) if item.quantity == 0: diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index b17602a..025c161 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -366,7 +366,7 @@ class UserEventInfo: conditions = [] for x in self.conditionInfo: conditions.append(x.make()) - + return [self.eventId, conditions] @@ -374,7 +374,7 @@ class UserEventConditionInfo: def __init__(self) -> None: self.achievementCondition = 0 self.progress = 0 - + def make(self) -> List: return [self.achievementCondition, self.progress] @@ -835,11 +835,13 @@ class LastSongDetail: class FriendScoreDetail: - def __init__(self, song_id: int = 0, difficulty: int = 1, best_score: int = 0) -> None: + def __init__( + self, song_id: int = 0, difficulty: int = 1, best_score: int = 0 + ) -> None: self.songId = song_id self.difficulty = difficulty self.bestScore = best_score - + def make(self) -> List: return [self.songId, self.difficulty, self.bestScore] diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index bf6b74b..b70ac35 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -11,7 +11,7 @@ class UserInfoUpdateRequest(BaseRequest): self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] self.unknown2: List = self.params[2] - self.datesUpdated: List[DateUpdate] = [] + self.datesUpdated: List[DateUpdate] = [] self.favoritesRemoved: List[int] = self.params[4] self.favoritesAdded: List[int] = self.params[5] diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index b7ed1fc..6eef16a 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -168,7 +168,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) - + for x in self.eventInfo: evts.append(x.make()) @@ -267,7 +267,9 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2): self, is_first_login_daily: bool = False, last_login_date: int = 0 ) -> None: super().__init__(is_first_login_daily, last_login_date) - self.unk: List = [] # Ticket info, item info, message, title, voice name (not sure how they fit...) + self.unk: List = ( + [] + ) # Ticket info, item info, message, title, voice name (not sure how they fit...) def make(self) -> Dict: super().make() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 4ff942b..6ac60de 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -73,7 +73,7 @@ class WaccaLily(WaccaS): self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() - + opts = self.data.profile.get_options(req.aimeId) self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") @@ -139,7 +139,7 @@ class WaccaLily(WaccaS): if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + for opt in opts: resp.options.append(UserOption(opt["opt_id"], opt["value"])) @@ -165,17 +165,23 @@ class WaccaLily(WaccaS): self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - midnight_today_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + midnight_today_ts = int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today if last_login_time < midnight_today_ts: resp.firstLoginDaily = True - - # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak if midnight_today_ts - last_login_time > 86400: is_consec_day = False - self.data.profile.session_login(req.userId, resp.firstLoginDaily, is_consec_day) + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month resp.vipInfo.pageDay = datetime.now().day diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index bb8a332..1711013 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -141,7 +141,7 @@ class WaccaReverse(WaccaLilyR): ) # For some fucking reason if this isn't here time play is disabled - resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) + resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) From 9d8762d3da67ecf3664018a00ede80ed5642e969 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 30 Apr 2023 01:18:00 +0000 Subject: [PATCH 36/36] Update 'readme.md' --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b1cb506..ec25191 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar. # Supported games -Games listed below have been tested and confirmed working. Only game versions older then the current one in active use in arcades (n-0) or current game versions older then a year (y-1) are supported. +Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. + Chunithm + All versions up to New!! Plus