diff --git a/core/allnet.py b/core/allnet.py index 67b47b3..0d4fbf7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -95,14 +95,21 @@ class AllnetServlet: self.logger.debug(f"Allnet request: {vars(req)}") if req.game_id not in self.uri_registry: - msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg - ) - self.logger.warn(msg) + if not self.config.server.is_develop: + msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + ) + self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = 0 + return self.dict_to_http_form_string([vars(resp)]) + + else: + self.logger.info(f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}") + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + return self.dict_to_http_form_string([vars(resp)]) resp.uri, resp.host = self.uri_registry[req.game_id] @@ -181,7 +188,9 @@ class AllnetServlet: self.logger.error(e) return b"" + self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}") resp = AllnetDownloadOrderResponse() + if not self.config.allnet.allow_online_updates: return self.dict_to_http_form_string([vars(resp)]) @@ -190,7 +199,7 @@ class AllnetServlet: def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) - request_ip = request.getClientAddress() + request_ip = Utils.get_ip_addr(request) if req_dict is None: self.logger.error(f"Failed to parse request {request.content.getvalue()}") return b"" @@ -223,7 +232,7 @@ class AllnetServlet: return self.dict_to_http_form_string([vars(resp)]) msg = ( - f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " + f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" ) self.logger.info(msg) diff --git a/core/data/database.py b/core/data/database.py index 0ce039e..07fe79e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -145,25 +145,49 @@ class Data: ) return - if not os.path.exists( - f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql" - ): - self.logger.error( - f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder" - ) - return + if action == "upgrade": + for x in range(old_ver, version): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder" + ) + return - with open( - f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() + with open( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() - result = self.base.execute(sql) - if result is None: - self.logger.error("Error execuing sql script!") - return None + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None + + else: + for x in range(old_ver, version, -1): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder" + ) + return + + with open( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None result = self.base.set_schema_ver(version, game) if result is None: @@ -237,3 +261,19 @@ class Data: if not cards: self.logger.info(f"Delete hanging user {user['id']}") self.user.delete_user(user["id"]) + + def autoupgrade(self) -> None: + all_games = self.base.get_all_schema_vers() + if all_games is None: + self.logger.warn("Failed to get schema versions") + + for x in all_games: + game = x["game"].upper() + update_ver = 1 + for y in range(2, 100): + if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): + update_ver = y + else: + break + + self.migrate_database(game, update_ver, "upgrade") \ No newline at end of file diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 9899f29..f77a9aa 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -2,6 +2,7 @@ import json import logging from random import randrange from typing import Any, Optional, Dict, List +from sqlalchemy.engine import Row from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select @@ -80,6 +81,14 @@ class BaseData: Generate a random 5-7 digit id """ return randrange(10000, 9999999) + + def get_all_schema_vers(self) -> Optional[List[Row]]: + sql = select(schema_ver) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def get_schema_ver(self, game: str) -> Optional[int]: sql = select(schema_ver).where(schema_ver.c.game == game) diff --git a/core/data/schema/versions/SBZV_1_rollback.sql b/core/data/schema/versions/SBZV_1_rollback.sql index a7bccce..6389157 100644 --- a/core/data/schema/versions/SBZV_1_rollback.sql +++ b/core/data/schema/versions/SBZV_1_rollback.sql @@ -1,9 +1,9 @@ SET FOREIGN_KEY_CHECKS=0; -ALTER TABLE diva_score DROP COLUMN edition; -ALTER TABLE diva_playlog DROP COLUMN edition; - ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty); ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; + +ALTER TABLE diva_score DROP COLUMN edition; +ALTER TABLE diva_playlog DROP COLUMN edition; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_rollback.sql b/core/data/schema/versions/SDEZ_2_rollback.sql new file mode 100644 index 0000000..b631711 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_rollback.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN cardId card_id INT NOT NULL AFTER user, +CHANGE COLUMN cardTypeId card_kind INT NOT NULL, +CHANGE COLUMN charaId chara_id INT NOT NULL, +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN itemId item_id INT NOT NULL AFTER user, +CHANGE COLUMN itemKind item_kind INT NOT NULL, +CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN characterId character_id INT NOT NULL, +CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN chargeId charge_id INT NOT NULL, +CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL, +CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_3_upgrade.sql b/core/data/schema/versions/SDEZ_3_upgrade.sql index c13e1fe..bdb5906 100644 --- a/core/data/schema/versions/SDEZ_3_upgrade.sql +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -3,8 +3,8 @@ CHANGE COLUMN card_id cardId INT NOT NULL AFTER user, CHANGE COLUMN card_kind cardTypeId INT NOT NULL, CHANGE COLUMN chara_id charaId INT NOT NULL, CHANGE COLUMN map_id mapId INT NOT NULL, -CHANGE COLUMN startDate startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', -CHANGE COLUMN endDate endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; +CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN end_date endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; ALTER TABLE mai2_item_item CHANGE COLUMN item_id itemId INT NOT NULL AFTER user, diff --git a/core/frontend.py b/core/frontend.py index e230681..127b174 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -30,7 +30,7 @@ class UserSession(object): class FrontendServlet(resource.Resource): def getChild(self, name: bytes, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}") if name == b"": return self return resource.Resource.getChild(self, name, request) @@ -84,7 +84,7 @@ class FrontendServlet(resource.Resource): ) def render_GET(self, request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") template = self.environment.get_template("core/frontend/index.jinja") return template.render( server_name=self.config.server.name, @@ -113,7 +113,7 @@ class FE_Base(resource.Resource): class FE_Gate(FE_Base): def render_GET(self, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") uri: str = request.uri.decode() sesh = request.getSession() diff --git a/dbutils.py b/dbutils.py index 65b1f8e..176c67e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -64,6 +64,9 @@ if __name__ == "__main__": else: data.migrate_database(args.game, int(args.version), args.action) + elif args.action == "autoupgrade": + data.autoupgrade() + elif args.action == "create-owner": data.create_owner(args.email) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 8af5541..b09d61e 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -8,13 +8,13 @@ using the megaime database. Clean installations always create the latest databas # Table of content -- [Supported Games](#Supported-Games) - - [Chunithm](#Chunithm) - - [crossbeats REV.](#crossbeats-REV) - - [maimai DX](#maimai-DX) - - [O.N.G.E.K.I.](#ONGEKI) - - [Card Maker](#Card-Maker) - - [WACCA](#WACCA) +- [Supported Games](#supported-games) + - [Chunithm](#chunithm) + - [crossbeats REV.](#crossbeats-rev) + - [maimai DX](#maimai-dx) + - [O.N.G.E.K.I.](#o-n-g-e-k-i) + - [Card Maker](#card-maker) + - [WACCA](#wacca) # Supported Games diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 3794f06..bbac976 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,5 +2,19 @@ server: enable: True loglevel: "info" +team: + name: ARTEMiS + +mods: + use_login_bonus: True + +version: + 11: + rom: 2.00.00 + data: 2.00.00 + 12: + rom: 2.05.00 + data: 2.05.00 + crypto: encrypted_only: False \ No newline at end of file diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 7256b10..89cd4f5 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 1 +current_schema_version = 3 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 13f423b..0eaabff 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -23,7 +23,98 @@ class ChuniBase: self.version = ChuniConstants.VER_CHUNITHM def handle_game_login_api_request(self, data: Dict) -> Dict: - # self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) + """ + Handles the login bonus logic, required for the game because + getUserLoginBonus gets called after getUserItem and therefore the + items needs to be inserted in the database before they get requested. + + Adds a bonusCount after a user logged in after 24 hours, makes sure + loginBonus 30 gets looped, only show the login banner every 24 hours, + adds the bonus to items (itemKind 6) + """ + + # ignore the login bonus if disabled in config + if not self.game_cfg.mods.use_login_bonus: + return {"returnCode": 1} + + user_id = data["userId"] + login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) + + for preset in login_bonus_presets: + # check if a user already has some pogress and if not add the + # login bonus entry + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + if user_login_bonus is None: + self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + # yeah i'm lazy + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + + # skip the login bonus entirely if its already finished + if user_login_bonus["isFinished"]: + continue + + # make sure the last login is more than 24 hours ago + if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta( + hours=24 + ): + # increase the login day counter and update the last login date + bonus_count = user_login_bonus["bonusCount"] + 1 + last_update_date = datetime.now() + + all_login_boni = self.data.static.get_login_bonus( + self.version, preset["id"] + ) + + # skip the current bonus preset if no boni were found + if all_login_boni is None or len(all_login_boni) < 1: + self.logger.warn( + f"No bonus entries found for bonus preset {preset['id']}" + ) + continue + + max_needed_days = all_login_boni[0]["needLoginDayCount"] + + # make sure to not show login boni after all days got redeemed + is_finished = False + if bonus_count > max_needed_days: + # assume that all login preset ids under 3000 needs to be + # looped, like 30 and 40 are looped, 40 does not work? + if preset["id"] < 3000: + bonus_count = 1 + else: + is_finished = True + + # grab the item for the corresponding day + login_item = self.data.static.get_login_bonus_by_required_days( + self.version, preset["id"], bonus_count + ) + if login_item is not None: + # now add the present to the database so the + # handle_get_user_item_api_request can grab them + self.data.item.put_item( + user_id, + { + "itemId": login_item["presentId"], + "itemKind": 6, + "stock": login_item["itemNum"], + "isValid": True, + }, + ) + + self.data.item.put_login_bonus( + user_id, + self.version, + preset["id"], + bonusCount=bonus_count, + lastUpdateDate=last_update_date, + isWatched=False, + isFinished=is_finished, + ) + return {"returnCode": 1} def handle_game_logout_api_request(self, data: Dict) -> Dict: @@ -33,6 +124,9 @@ class ChuniBase: def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) + if game_charge_list is None or len(game_charge_list) == 0: + return {"length": 0, "gameChargeList": []} + charges = [] for x in range(len(game_charge_list)): charges.append( @@ -52,6 +146,14 @@ class ChuniBase: def handle_get_game_event_api_request(self, data: Dict) -> Dict: game_events = self.data.static.get_enabled_events(self.version) + if game_events is None or len(game_events) == 0: + self.logger.warn("No enabled events, did you run the reader?") + return { + "type": data["type"], + "length": 0, + "gameEventList": [], + } + event_list = [] for evt_row in game_events: tmp = {} @@ -119,7 +221,7 @@ class ChuniBase: return { "userId": data["userId"], "length": len(activity_list), - "kind": data["kind"], + "kind": int(data["kind"]), "userActivityList": activity_list, } @@ -298,26 +400,29 @@ class ChuniBase: } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - """ - Unsure how to get this to trigger... - """ + user_id = data["userId"] + user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) + # ignore the loginBonus request if its disabled in config + if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: + return {"userId": user_id, "length": 0, "userLoginBonusList": []} + + user_login_list = [] + for bonus in user_login_bonus: + user_login_list.append( + { + "presetId": bonus["presetId"], + "bonusCount": bonus["bonusCount"], + "lastUpdateDate": datetime.strftime( + bonus["lastUpdateDate"], "%Y-%m-%d %H:%M:%S" + ), + "isWatched": bonus["isWatched"], + } + ) + return { - "userId": data["userId"], - "length": 2, - "userLoginBonusList": [ - { - "presetId": "10", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - { - "presetId": "20", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - ], + "userId": user_id, + "length": len(user_login_list), + "userLoginBonusList": user_login_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: @@ -440,13 +545,13 @@ class ChuniBase: "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": "1", + "chargeState": 1, "userNameEx": profile["userName"], } def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) - if recet_rating_list is None: + recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) + if recent_rating_list is None: return { "userId": data["userId"], "length": 0, @@ -455,8 +560,8 @@ class ChuniBase: return { "userId": data["userId"], - "length": len(recet_rating_list["recentRating"]), - "userRecentRatingList": recet_rating_list["recentRating"], + "length": len(recent_rating_list["recentRating"]), + "userRecentRatingList": recent_rating_list["recentRating"], } def handle_get_user_region_api_request(self, data: Dict) -> Dict: @@ -468,8 +573,24 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: Team - return {"userId": data["userId"], "teamId": 0} + # TODO: use the database "chuni_profile_team" with a GUI + team_name = self.game_cfg.team.team_name + if team_name == "": + return {"userId": data["userId"], "teamId": 0} + + return { + "userId": data["userId"], + "teamId": 1, + "teamRank": 1, + "teamName": team_name, + "userTeamPoint": { + "userId": data["userId"], + "teamId": 1, + "orderId": 1, + "teamPoint": 1, + "aggrDate": data["playDate"], + }, + } def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { @@ -569,9 +690,18 @@ class ChuniBase: for emoney in upsert["userEmoneyList"]: self.data.profile.put_profile_emoney(user_id, emoney) + if "userLoginBonusList" in upsert: + for login in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, self.version, login["presetId"], isWatched=True + ) + return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + # add tickets after they got bought, this makes sure the tickets are + # still valid after an unsuccessful logout + self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -592,7 +722,5 @@ class ChuniBase: def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], - "userNetBattleData": { - "recentNBSelectMusicList": [] - } - } \ No newline at end of file + "userNetBattleData": {"recentNBSelectMusicList": []}, + } diff --git a/titles/chuni/config.py b/titles/chuni/config.py index c4a351f..48d70d2 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -21,6 +21,42 @@ class ChuniServerConfig: ) +class ChuniTeamConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def team_name(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "name", default="" + ) + + +class ChuniModsConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def use_login_bonus(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "mods", "use_login_bonus", default=True + ) + + +class ChuniVersionConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + def version(self, version: int) -> Dict: + """ + in the form of: + 11: {"rom": 2.00.00, "data": 2.00.00} + """ + return CoreConfig.get_config_field( + self.__config, "chuni", "version", default={} + )[version] + + class ChuniCryptoConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config @@ -46,4 +82,7 @@ class ChuniCryptoConfig: class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) + self.team = ChuniTeamConfig(self) + self.mods = ChuniModsConfig(self) + self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 611c6d2..67b6fcc 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": "2.00.00", - "dataVersion": "2.00.00", + "romVersion": self.game_cfg.version.version(self.version)["rom"], + "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", @@ -269,10 +269,10 @@ class ChuniNew(ChuniBase): tmp = user_print_list[x]._asdict() print_list.append(tmp["cardId"]) - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: break - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: next_idx = next_idx + max_ct else: next_idx = -1 @@ -454,9 +454,7 @@ class ChuniNew(ChuniBase): # set the card print state to success and use the orderId as the key self.data.item.put_user_print_state( - user_id, - id=upsert["orderId"], - hasCompleted=True + user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} @@ -467,10 +465,6 @@ class ChuniNew(ChuniBase): # set the card print state to success and use the orderId as the key for order_id in order_ids: - self.data.item.put_user_print_state( - user_id, - id=order_id, - hasCompleted=True - ) + self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 9dec9aa..422d57a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -1,6 +1,4 @@ -from datetime import datetime, timedelta from typing import Dict, Any -import pytz from core.config import CoreConfig from titles.chuni.new import ChuniNew @@ -15,8 +13,8 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = "2.05.00" - ret["gameSetting"]["dataVersion"] = "2.05.00" + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/read.py b/titles/chuni/read.py index abf78e3..9ac13c4 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -42,6 +42,80 @@ class ChuniReader(BaseReader): self.read_music(f"{dir}/music") self.read_charges(f"{dir}/chargeItem") self.read_avatar(f"{dir}/avatarAccessory") + self.read_login_bonus(f"{dir}/") + + def read_login_bonus(self, root_dir: str) -> None: + for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): + for dir in dirs: + if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): + with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + xml_root = ET.fromstring(strdata) + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + is_enabled = ( + True if xml_root.find("disableFlag").text == "false" else False + ) + + result = self.data.static.put_login_bonus_preset( + self.version, id, name, is_enabled + ) + + if result is not None: + self.logger.info(f"Inserted login bonus preset {id}") + else: + self.logger.warn(f"Failed to insert login bonus preset {id}") + + for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"): + for name in bonus.findall("loginBonusName"): + bonus_id = name.find("id").text + bonus_name = name.find("str").text + + if path.exists( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml" + ): + with open( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml", + "rb", + ) as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + bonus_root = ET.fromstring(strdata) + + for present in bonus_root.findall("present"): + present_id = present.find("id").text + present_name = present.find("str").text + + item_num = int(bonus_root.find("itemNum").text) + need_login_day_count = int( + bonus_root.find("needLoginDayCount").text + ) + login_bonus_category_type = int( + bonus_root.find("loginBonusCategoryType").text + ) + + result = self.data.static.put_login_bonus( + self.version, + id, + bonus_id, + bonus_name, + present_id, + present_name, + item_num, + need_login_day_count, + login_bonus_category_type, + ) + + if result is not None: + self.logger.info(f"Inserted login bonus {bonus_id}") + else: + self.logger.warn( + f"Failed to insert login bonus {bonus_id}" + ) def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 124d7df..4ffcf93 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -184,8 +184,73 @@ print_detail = Table( mysql_charset="utf8mb4", ) +login_bonus = Table( + "chuni_item_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("presetId", Integer, nullable=False), + Column("bonusCount", Integer, nullable=False, server_default="0"), + Column("lastUpdateDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("isWatched", Boolean, server_default="0"), + Column("isFinished", Boolean, server_default="0"), + UniqueConstraint("version", "user", "presetId", name="chuni_item_login_bonus_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def put_login_bonus( + self, user_id: int, version: int, preset_id: int, **login_bonus_data + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, user=user_id, presetId=preset_id, **login_bonus_data + ) + + conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_all_login_bonus( + self, user_id: int, version: int, is_finished: bool = False + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.isFinished == is_finished, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus( + self, user_id: int, version: int, preset_id: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.presetId == preset_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id @@ -335,7 +400,7 @@ class ChuniItemData(BaseData): sql = print_state.select( and_( print_state.c.user == aime_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -351,7 +416,7 @@ class ChuniItemData(BaseData): and_( print_state.c.user == aime_id, print_state.c.gachaId == gacha_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -380,9 +445,7 @@ class ChuniItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: @@ -390,4 +453,4 @@ class ChuniItemData(BaseData): f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" ) return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9000b9b..e35769c 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -558,8 +558,10 @@ class ChuniProfileData(BaseData): return result.lastrowid def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: - sql = select(activity).where( - and_(activity.c.user == aime_id, activity.c.kind == kind) + sql = ( + select(activity) + .where(and_(activity.c.user == aime_id, activity.c.kind == kind)) + .order_by(activity.c.sortNumber.desc()) # to get the last played track ) result = self.execute(sql) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0784872..bef58c0 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -122,8 +122,148 @@ gacha_cards = Table( mysql_charset="utf8mb4", ) +login_bonus_preset = Table( + "chuni_static_login_bonus_preset", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presetName", String(255), nullable=False), + Column("isEnabled", Boolean, server_default="1"), + UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + mysql_charset="utf8mb4", +) + +login_bonus = Table( + "chuni_static_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column( + "presetId", + ForeignKey( + "chuni_static_login_bonus_preset.id", + ondelete="cascade", + onupdate="cascade", + ), + nullable=False, + ), + Column("loginBonusId", Integer, nullable=False), + Column("loginBonusName", String(255), nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("itemNum", Integer, nullable=False), + Column("needLoginDayCount", Integer, nullable=False), + Column("loginBonusCategoryType", Integer, nullable=False), + UniqueConstraint( + "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): + def put_login_bonus( + self, + version: int, + preset_id: int, + login_bonus_id: int, + login_bonus_name: str, + present_id: int, + present_ame: str, + item_num: int, + need_login_day_count: int, + login_bonus_category_type: int, + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, + presetId=preset_id, + loginBonusId=login_bonus_id, + loginBonusName=login_bonus_name, + presentId=present_id, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + conflict = sql.on_duplicate_key_update( + loginBonusName=login_bonus_name, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus( + self, version: int, preset_id: int, + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + ) + ).order_by(login_bonus.c.needLoginDayCount.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus_by_required_days( + self, version: int, preset_id: int, need_login_day_count: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + login_bonus.c.needLoginDayCount == need_login_day_count, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_login_bonus_preset( + self, version: int, preset_id: int, preset_name: str, is_enabled: bool + ) -> Optional[int]: + sql = insert(login_bonus_preset).values( + id=preset_id, + version=version, + presetName=preset_name, + isEnabled=is_enabled, + ) + + conflict = sql.on_duplicate_key_update( + presetName=preset_name, isEnabled=is_enabled + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus_presets( + self, version: int, is_enabled: bool = True + ) -> Optional[List[Row]]: + sql = login_bonus_preset.select( + and_( + login_bonus_preset.c.version == version, + login_bonus_preset.c.isEnabled == is_enabled, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: @@ -390,20 +530,17 @@ class ChuniStaticData(BaseData): return None return result.fetchall() - def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + def get_gacha_card_by_character( + self, gacha_id: int, chara_id: int + ) -> Optional[Dict]: sql_sub = ( - select(cards.c.cardId) - .filter( - cards.c.charaId == chara_id - ) - .scalar_subquery() + select(cards.c.cardId).filter(cards.c.charaId == chara_id).scalar_subquery() ) # Perform the main query, also rename the resulting column to ranking - sql = gacha_cards.select(and_( - gacha_cards.c.gachaId == gacha_id, - gacha_cards.c.cardId == sql_sub - )) + sql = gacha_cards.select( + and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub) + ) result = self.execute(sql) if result is None: diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 6c2bf26..1a9d5da 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta import json, logging from typing import Any, Dict +import random -from core.config import CoreConfig +from core.data import Data +from core import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.proto import jackal_pb2 @@ -13,6 +15,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") + self.data = Data(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -123,7 +126,94 @@ class PokkenBase: ranking.event_end = True ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) + return res.SerializeToString() + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_USER + access_code = request.load_user.access_code + user_id = self.data.card.get_user_id_from_card(access_code) + + if user_id is None: # TODO: Toggle auto-register + user_id = self.data.user.create_user() + card_id = self.data.card.create_card(user_id, access_code) + + self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") + + # TODO: Check for user data. For now just treat ever card-in as a new user + + load_usr = jackal_pb2.LoadUserResponseData() + load_usr.commidserv_result = 1 + load_usr.load_hash = 1 + load_usr.cardlock_status = False + load_usr.banapass_id = user_id + load_usr.access_code = access_code + load_usr.new_card_flag = True + load_usr.precedent_release_flag = 0xFFFFFFFF + load_usr.navi_newbie_flag = True + load_usr.navi_enable_flag = True + load_usr.pad_vibrate_flag = True + load_usr.home_region_code = 0 + load_usr.home_loc_name = "" + load_usr.pref_code = 0 + load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) + load_usr.trainer_rank_point = 0 + load_usr.wallet = 0 + load_usr.fight_money = 0 + load_usr.score_point = 0 + load_usr.grade_max_num = 0 + load_usr.total_play_days = 0 + load_usr.play_date_time = 0 + load_usr.lucky_box_fail_num = 0 + load_usr.event_reward_get_flag = 0 + load_usr.rank_pvp_all = 0 + load_usr.rank_pvp_loc = 0 + load_usr.rank_cpu_all = 0 + load_usr.rank_cpu_loc = 0 + load_usr.rank_event = 0 + load_usr.awake_num = 0 + load_usr.use_support_num = 0 + load_usr.rankmatch_flag = 0 + load_usr.title_text_id = 0 + load_usr.title_plate_id = 0 + load_usr.title_decoration_id = 0 + load_usr.navi_trainer = 0 + load_usr.navi_version_id = 0 + load_usr.aid_skill = 0 + load_usr.comment_text_id = 0 + load_usr.comment_word_id = 0 + load_usr.latest_use_pokemon = 0 + load_usr.ex_ko_num = 0 + load_usr.wko_num = 0 + load_usr.timeup_win_num = 0 + load_usr.cool_ko_num = 0 + load_usr.perfect_ko_num = 0 + load_usr.record_flag = 0 + load_usr.site_register_status = 0 + load_usr.continue_num = 0 + load_usr.event_state = 0 + load_usr.event_id = 0 + load_usr.sp_bonus_category_id_1 = 0 + load_usr.sp_bonus_key_value_1 = 0 + load_usr.sp_bonus_category_id_2 = 0 + load_usr.sp_bonus_key_value_2 = 0 + + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 42a5a0e..a7a328f 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -124,7 +124,6 @@ class PokkenServlet(resource.Resource): self.logger.debug(pokken_request) ret = handler(pokken_request) - self.logger.debug(f"Response: {ret}") return ret def handle_matching(self, request: Request) -> bytes: diff --git a/titles/wacca/base.py b/titles/wacca/base.py index bc0c09a..020c167 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -635,14 +635,14 @@ class WaccaBase: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"]: + if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp: if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() - elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: + elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets: for x in range(len(new_tickets)): if new_tickets[x][1] == item.itemId: self.data.item.spend_ticket(new_tickets[x][0]) @@ -836,7 +836,7 @@ class WaccaBase: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - + resp.songDetail.lock_state = 1 return resp.make() # TODO: Coop and vs data @@ -880,7 +880,7 @@ class WaccaBase: user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP: + if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp: resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) @@ -1070,19 +1070,12 @@ class WaccaBase: ): if item.quantity > WaccaConstants.Difficulty.HARD.value: old_score = self.data.score.get_best_score( - user_id, item.itemId, item.quantity + user_id, item.itemId, item.quantity + ) + if not old_score: + self.data.score.put_best_score( + user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0 ) - if not old_score: - self.data.score.put_best_score( - user_id, - item.itemId, - item.quantity, - 0, - [0] * 5, - [0] * 13, - 0, - 0, - ) if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 9e9847a..f86be2f 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -577,7 +577,21 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): self.ssspCt = counts[12] def make(self) -> List: - return super().make() + [self.spCt, self.sspCt, self.ssspCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.spCt, + self.ssCt, + self.sspCt, + self.sssCt, + self.ssspCt, + self.masterCt, + ] class BestScoreDetailV1: @@ -928,7 +942,7 @@ class MusicUpdateDetailV1: self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.locked = 0 + self.lock_state = 0 def make(self) -> List: return [ @@ -940,7 +954,7 @@ class MusicUpdateDetailV1: self.score, self.lowestMissCount, self.maxSkillPts, - self.locked, + self.lock_state, ] diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 8c67db9..bf6b74b 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -11,9 +11,9 @@ class UserInfoUpdateRequest(BaseRequest): self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] self.unknown2: List = self.params[2] - self.datesUpdated: List[DateUpdate] = [] - self.favoritesAdded: List[int] = self.params[4] - self.favoritesRemoved: List[int] = self.params[5] + self.datesUpdated: List[DateUpdate] = [] + self.favoritesRemoved: List[int] = self.params[4] + self.favoritesAdded: List[int] = self.params[5] for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 0b51849..a59cda1 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -102,7 +102,7 @@ class WaccaServlet: resp.message = "不正なリクエスト エラーです" return end(resp.make()) - if "/api/" in url_path: + if "api/" in url_path: func_to_find = ( "handle_" + url_path.partition("api/")[2].replace("/", "_") + "_request" )