From 2a290f2a3df154e39c29d0067f230532b705241c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 24 Mar 2023 18:10:10 +0100 Subject: [PATCH 01/10] chuni: added teams and ticket saving, fixed last played song --- example_config/chuni.yaml | 11 +++++++++ titles/chuni/__init__.py | 2 +- titles/chuni/base.py | 43 ++++++++++++++++++++++++---------- titles/chuni/config.py | 28 ++++++++++++++++++++++ titles/chuni/new.py | 14 ++++------- titles/chuni/newplus.py | 10 ++++---- titles/chuni/schema/profile.py | 6 +++-- titles/chuni/schema/static.py | 17 ++++++-------- 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 3794f06..c70ccab 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,5 +2,16 @@ server: enable: True loglevel: "info" +team: + name: ARTEMiS + +version: + "11": + rom: 2.00.00 + data: 2.00.00 + "12": + rom: 2.05.00 + data: 2.05.00 + crypto: encrypted_only: False \ No newline at end of file diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 7256b10..89cd4f5 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 1 +current_schema_version = 3 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 3668b29..936e5ef 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -32,7 +32,7 @@ class ChuniBase: def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) - + if game_charge_list is None or len(game_charge_list) == 0: return {"length": 0, "gameChargeList": []} @@ -130,7 +130,7 @@ class ChuniBase: return { "userId": data["userId"], "length": len(activity_list), - "kind": data["kind"], + "kind": int(data["kind"]), "userActivityList": activity_list, } @@ -451,13 +451,13 @@ class ChuniBase: "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": "1", + "chargeState": 1, "userNameEx": profile["userName"], } def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) - if recet_rating_list is None: + recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) + if recent_rating_list is None: return { "userId": data["userId"], "length": 0, @@ -466,8 +466,8 @@ class ChuniBase: return { "userId": data["userId"], - "length": len(recet_rating_list["recentRating"]), - "userRecentRatingList": recet_rating_list["recentRating"], + "length": len(recent_rating_list["recentRating"]), + "userRecentRatingList": recent_rating_list["recentRating"], } def handle_get_user_region_api_request(self, data: Dict) -> Dict: @@ -479,8 +479,24 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: Team - return {"userId": data["userId"], "teamId": 0} + # TODO: use the database "chuni_profile_team" with a GUI + team_name = self.game_cfg.team.team_name + if team_name == "": + return {"userId": data["userId"], "teamId": 0} + + return { + "userId": data["userId"], + "teamId": 1, + "teamRank": 1, + "teamName": team_name, + "userTeamPoint": { + "userId": data["userId"], + "teamId": 1, + "orderId": 1, + "teamPoint": 1, + "aggrDate": data["playDate"], + }, + } def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { @@ -583,6 +599,9 @@ class ChuniBase: return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + # add tickets after they got bought, this makes sure the tickets are + # still valid after an unsuccessful logout + self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -603,7 +622,5 @@ class ChuniBase: def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], - "userNetBattleData": { - "recentNBSelectMusicList": [] - } - } \ No newline at end of file + "userNetBattleData": {"recentNBSelectMusicList": []}, + } diff --git a/titles/chuni/config.py b/titles/chuni/config.py index c4a351f..ac527af 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -21,6 +21,32 @@ class ChuniServerConfig: ) +class ChuniTeamConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def team_name(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "name", default="" + ) + + +class ChuniVersionConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + def version_rom(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" + ) + + def version_data(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" + ) + + class ChuniCryptoConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config @@ -46,4 +72,6 @@ class ChuniCryptoConfig: class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) + self.team = ChuniTeamConfig(self) + self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 611c6d2..2c50c07 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": "2.00.00", - "dataVersion": "2.00.00", + "romVersion": self.game_cfg.version.version_rom(self.version), + "dataVersion": self.game_cfg.version.version_data(self.version), "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", @@ -454,9 +454,7 @@ class ChuniNew(ChuniBase): # set the card print state to success and use the orderId as the key self.data.item.put_user_print_state( - user_id, - id=upsert["orderId"], - hasCompleted=True + user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} @@ -467,10 +465,6 @@ class ChuniNew(ChuniBase): # set the card print state to success and use the orderId as the key for order_id in order_ids: - self.data.item.put_user_print_state( - user_id, - id=order_id, - hasCompleted=True - ) + self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 9dec9aa..83debb5 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -1,6 +1,4 @@ -from datetime import datetime, timedelta from typing import Dict, Any -import pytz from core.config import CoreConfig from titles.chuni.new import ChuniNew @@ -15,8 +13,12 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = "2.05.00" - ret["gameSetting"]["dataVersion"] = "2.05.00" + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version_rom( + self.version + ) + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( + self.version + ) ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9000b9b..e35769c 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -558,8 +558,10 @@ class ChuniProfileData(BaseData): return result.lastrowid def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: - sql = select(activity).where( - and_(activity.c.user == aime_id, activity.c.kind == kind) + sql = ( + select(activity) + .where(and_(activity.c.user == aime_id, activity.c.kind == kind)) + .order_by(activity.c.sortNumber.desc()) # to get the last played track ) result = self.execute(sql) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0784872..4882046 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -390,20 +390,17 @@ class ChuniStaticData(BaseData): return None return result.fetchall() - def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + def get_gacha_card_by_character( + self, gacha_id: int, chara_id: int + ) -> Optional[Dict]: sql_sub = ( - select(cards.c.cardId) - .filter( - cards.c.charaId == chara_id - ) - .scalar_subquery() + select(cards.c.cardId).filter(cards.c.charaId == chara_id).scalar_subquery() ) # Perform the main query, also rename the resulting column to ranking - sql = gacha_cards.select(and_( - gacha_cards.c.gachaId == gacha_id, - gacha_cards.c.cardId == sql_sub - )) + sql = gacha_cards.select( + and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub) + ) result = self.execute(sql) if result is None: From 6489e3ca21b2e3473ead2e029b80b19dc5c453e2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 26 Mar 2023 04:33:53 -0400 Subject: [PATCH 02/10] pokken: add skeleton LoadUser response --- titles/pokken/base.py | 92 +++++++++++++++++++++++++++++++++++++++++- titles/pokken/index.py | 1 - 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 6c2bf26..1a9d5da 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta import json, logging from typing import Any, Dict +import random -from core.config import CoreConfig +from core.data import Data +from core import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.proto import jackal_pb2 @@ -13,6 +15,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") + self.data = Data(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -123,7 +126,94 @@ class PokkenBase: ranking.event_end = True ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) + return res.SerializeToString() + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_USER + access_code = request.load_user.access_code + user_id = self.data.card.get_user_id_from_card(access_code) + + if user_id is None: # TODO: Toggle auto-register + user_id = self.data.user.create_user() + card_id = self.data.card.create_card(user_id, access_code) + + self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") + + # TODO: Check for user data. For now just treat ever card-in as a new user + + load_usr = jackal_pb2.LoadUserResponseData() + load_usr.commidserv_result = 1 + load_usr.load_hash = 1 + load_usr.cardlock_status = False + load_usr.banapass_id = user_id + load_usr.access_code = access_code + load_usr.new_card_flag = True + load_usr.precedent_release_flag = 0xFFFFFFFF + load_usr.navi_newbie_flag = True + load_usr.navi_enable_flag = True + load_usr.pad_vibrate_flag = True + load_usr.home_region_code = 0 + load_usr.home_loc_name = "" + load_usr.pref_code = 0 + load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) + load_usr.trainer_rank_point = 0 + load_usr.wallet = 0 + load_usr.fight_money = 0 + load_usr.score_point = 0 + load_usr.grade_max_num = 0 + load_usr.total_play_days = 0 + load_usr.play_date_time = 0 + load_usr.lucky_box_fail_num = 0 + load_usr.event_reward_get_flag = 0 + load_usr.rank_pvp_all = 0 + load_usr.rank_pvp_loc = 0 + load_usr.rank_cpu_all = 0 + load_usr.rank_cpu_loc = 0 + load_usr.rank_event = 0 + load_usr.awake_num = 0 + load_usr.use_support_num = 0 + load_usr.rankmatch_flag = 0 + load_usr.title_text_id = 0 + load_usr.title_plate_id = 0 + load_usr.title_decoration_id = 0 + load_usr.navi_trainer = 0 + load_usr.navi_version_id = 0 + load_usr.aid_skill = 0 + load_usr.comment_text_id = 0 + load_usr.comment_word_id = 0 + load_usr.latest_use_pokemon = 0 + load_usr.ex_ko_num = 0 + load_usr.wko_num = 0 + load_usr.timeup_win_num = 0 + load_usr.cool_ko_num = 0 + load_usr.perfect_ko_num = 0 + load_usr.record_flag = 0 + load_usr.site_register_status = 0 + load_usr.continue_num = 0 + load_usr.event_state = 0 + load_usr.event_id = 0 + load_usr.sp_bonus_category_id_1 = 0 + load_usr.sp_bonus_key_value_1 = 0 + load_usr.sp_bonus_category_id_2 = 0 + load_usr.sp_bonus_key_value_2 = 0 + + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 42a5a0e..a7a328f 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -124,7 +124,6 @@ class PokkenServlet(resource.Resource): self.logger.debug(pokken_request) ret = handler(pokken_request) - self.logger.debug(f"Response: {ret}") return ret def handle_matching(self, request: Request) -> bytes: From 541fe76a7c5b02b2b06de99aaf14fa5ce565eebd Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 01:09:16 +0200 Subject: [PATCH 03/10] cardmaker: fixed chuni endless loading --- titles/chuni/new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2c50c07..2963850 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -269,10 +269,10 @@ class ChuniNew(ChuniBase): tmp = user_print_list[x]._asdict() print_list.append(tmp["cardId"]) - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: break - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: next_idx = next_idx + max_ct else: next_idx = -1 From 1aa92458f447b36bfef4e52ddf0547728f136383 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:28:57 +0200 Subject: [PATCH 04/10] chuni: added login bonus (+importer), fixed config strings --- example_config/chuni.yaml | 7 +- titles/chuni/base.py | 133 +++++++++++++++++++++++++++----- titles/chuni/config.py | 27 +++++-- titles/chuni/new.py | 4 +- titles/chuni/newplus.py | 8 +- titles/chuni/read.py | 74 ++++++++++++++++++ titles/chuni/schema/item.py | 75 ++++++++++++++++-- titles/chuni/schema/static.py | 140 ++++++++++++++++++++++++++++++++++ 8 files changed, 424 insertions(+), 44 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index c70ccab..bbac976 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -5,11 +5,14 @@ server: team: name: ARTEMiS +mods: + use_login_bonus: True + version: - "11": + 11: rom: 2.00.00 data: 2.00.00 - "12": + 12: rom: 2.05.00 data: 2.05.00 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 936e5ef..95c35e8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -23,7 +23,92 @@ class ChuniBase: self.version = ChuniConstants.VER_CHUNITHM def handle_game_login_api_request(self, data: Dict) -> Dict: - # self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) + """ + Handles the login bonus logic, required for the game because + getUserLoginBonus gets called after getUserItem and therefore the + items needs to be inserted in the database before they get requested. + + Adds a bonusCount after a user logged in after 24 hours, makes sure + loginBonus 30 gets looped, only show the login banner every 24 hours, + adds the bonus to items (itemKind 6) + """ + + # ignore the login bonus if disabled in config + if not self.game_cfg.mods.use_login_bonus: + return {"returnCode": 1} + + user_id = data["userId"] + login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) + + for preset in login_bonus_presets: + # check if a user already has some pogress and if not add the + # login bonus entry + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + if user_login_bonus is None: + self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + # yeah i'm lazy + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + + # skip the login bonus entirely if its already finished + if user_login_bonus["isFinished"]: + continue + + # make sure the last login is more than 24 hours ago + if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta( + hours=24 + ): + # increase the login day counter and update the last login date + bonus_count = user_login_bonus["bonusCount"] + 1 + last_update_date = datetime.now() + + all_login_boni = self.data.static.get_login_bonus( + self.version, preset["id"] + ) + + # assume its not None + max_needed_days = all_login_boni[0]["needLoginDayCount"] + + # make sure to not show login boni after all days got redeemed + is_finished = False + if bonus_count > max_needed_days: + # assume that all login preset ids under 3000 needs to be + # looped, like 30 and 40 are looped, 40 does not work? + if preset["id"] < 3000: + bonus_count = 1 + else: + is_finished = True + + # grab the item for the corresponding day + login_item = self.data.static.get_login_bonus_by_required_days( + self.version, preset["id"], bonus_count + ) + if login_item is not None: + # now add the present to the database so the + # handle_get_user_item_api_request can grab them + self.data.item.put_item( + user_id, + { + "itemId": login_item["presentId"], + "itemKind": 6, + "stock": login_item["itemNum"], + "isValid": True, + }, + ) + + self.data.item.put_login_bonus( + user_id, + self.version, + preset["id"], + bonusCount=bonus_count, + lastUpdateDate=last_update_date, + isWatched=False, + isFinished=is_finished, + ) + return {"returnCode": 1} def handle_game_logout_api_request(self, data: Dict) -> Dict: @@ -309,26 +394,28 @@ class ChuniBase: } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - """ - Unsure how to get this to trigger... - """ + user_id = data["userId"] + user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) + if user_login_bonus is None: + return {"userId": user_id, "length": 0, "userLoginBonusList": []} + + user_login_list = [] + for bonus in user_login_bonus: + user_login_list.append( + { + "presetId": bonus["presetId"], + "bonusCount": bonus["bonusCount"], + "lastUpdateDate": datetime.strftime( + bonus["lastUpdateDate"], "%Y-%m-%d %H:%M:%S" + ), + "isWatched": bonus["isWatched"], + } + ) + return { - "userId": data["userId"], - "length": 2, - "userLoginBonusList": [ - { - "presetId": "10", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - { - "presetId": "20", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - ], + "userId": user_id, + "length": len(user_login_list), + "userLoginBonusList": user_login_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: @@ -596,6 +683,12 @@ class ChuniBase: for emoney in upsert["userEmoneyList"]: self.data.profile.put_profile_emoney(user_id, emoney) + if "userLoginBonusList" in upsert: + for login in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, self.version, login["presetId"], isWatched=True + ) + return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: diff --git a/titles/chuni/config.py b/titles/chuni/config.py index ac527af..48d70d2 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -32,19 +32,29 @@ class ChuniTeamConfig: ) +class ChuniModsConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def use_login_bonus(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "mods", "use_login_bonus", default=True + ) + + class ChuniVersionConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config - def version_rom(self, version: int) -> str: + def version(self, version: int) -> Dict: + """ + in the form of: + 11: {"rom": 2.00.00, "data": 2.00.00} + """ return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" - ) - - def version_data(self, version: int) -> str: - return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" - ) + self.__config, "chuni", "version", default={} + )[version] class ChuniCryptoConfig: @@ -73,5 +83,6 @@ class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) self.team = ChuniTeamConfig(self) + self.mods = ChuniModsConfig(self) self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2963850..67b6fcc 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": self.game_cfg.version.version_rom(self.version), - "dataVersion": self.game_cfg.version.version_data(self.version), + "romVersion": self.game_cfg.version.version(self.version)["rom"], + "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 83debb5..422d57a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -13,12 +13,8 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = self.game_cfg.version.version_rom( - self.version - ) - ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( - self.version - ) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/read.py b/titles/chuni/read.py index abf78e3..9ac13c4 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -42,6 +42,80 @@ class ChuniReader(BaseReader): self.read_music(f"{dir}/music") self.read_charges(f"{dir}/chargeItem") self.read_avatar(f"{dir}/avatarAccessory") + self.read_login_bonus(f"{dir}/") + + def read_login_bonus(self, root_dir: str) -> None: + for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): + for dir in dirs: + if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): + with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + xml_root = ET.fromstring(strdata) + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + is_enabled = ( + True if xml_root.find("disableFlag").text == "false" else False + ) + + result = self.data.static.put_login_bonus_preset( + self.version, id, name, is_enabled + ) + + if result is not None: + self.logger.info(f"Inserted login bonus preset {id}") + else: + self.logger.warn(f"Failed to insert login bonus preset {id}") + + for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"): + for name in bonus.findall("loginBonusName"): + bonus_id = name.find("id").text + bonus_name = name.find("str").text + + if path.exists( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml" + ): + with open( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml", + "rb", + ) as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + bonus_root = ET.fromstring(strdata) + + for present in bonus_root.findall("present"): + present_id = present.find("id").text + present_name = present.find("str").text + + item_num = int(bonus_root.find("itemNum").text) + need_login_day_count = int( + bonus_root.find("needLoginDayCount").text + ) + login_bonus_category_type = int( + bonus_root.find("loginBonusCategoryType").text + ) + + result = self.data.static.put_login_bonus( + self.version, + id, + bonus_id, + bonus_name, + present_id, + present_name, + item_num, + need_login_day_count, + login_bonus_category_type, + ) + + if result is not None: + self.logger.info(f"Inserted login bonus {bonus_id}") + else: + self.logger.warn( + f"Failed to insert login bonus {bonus_id}" + ) def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 124d7df..4ffcf93 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -184,8 +184,73 @@ print_detail = Table( mysql_charset="utf8mb4", ) +login_bonus = Table( + "chuni_item_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("presetId", Integer, nullable=False), + Column("bonusCount", Integer, nullable=False, server_default="0"), + Column("lastUpdateDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("isWatched", Boolean, server_default="0"), + Column("isFinished", Boolean, server_default="0"), + UniqueConstraint("version", "user", "presetId", name="chuni_item_login_bonus_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def put_login_bonus( + self, user_id: int, version: int, preset_id: int, **login_bonus_data + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, user=user_id, presetId=preset_id, **login_bonus_data + ) + + conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_all_login_bonus( + self, user_id: int, version: int, is_finished: bool = False + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.isFinished == is_finished, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus( + self, user_id: int, version: int, preset_id: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.presetId == preset_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id @@ -335,7 +400,7 @@ class ChuniItemData(BaseData): sql = print_state.select( and_( print_state.c.user == aime_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -351,7 +416,7 @@ class ChuniItemData(BaseData): and_( print_state.c.user == aime_id, print_state.c.gachaId == gacha_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -380,9 +445,7 @@ class ChuniItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: @@ -390,4 +453,4 @@ class ChuniItemData(BaseData): f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" ) return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 4882046..bef58c0 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -122,8 +122,148 @@ gacha_cards = Table( mysql_charset="utf8mb4", ) +login_bonus_preset = Table( + "chuni_static_login_bonus_preset", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presetName", String(255), nullable=False), + Column("isEnabled", Boolean, server_default="1"), + UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + mysql_charset="utf8mb4", +) + +login_bonus = Table( + "chuni_static_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column( + "presetId", + ForeignKey( + "chuni_static_login_bonus_preset.id", + ondelete="cascade", + onupdate="cascade", + ), + nullable=False, + ), + Column("loginBonusId", Integer, nullable=False), + Column("loginBonusName", String(255), nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("itemNum", Integer, nullable=False), + Column("needLoginDayCount", Integer, nullable=False), + Column("loginBonusCategoryType", Integer, nullable=False), + UniqueConstraint( + "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): + def put_login_bonus( + self, + version: int, + preset_id: int, + login_bonus_id: int, + login_bonus_name: str, + present_id: int, + present_ame: str, + item_num: int, + need_login_day_count: int, + login_bonus_category_type: int, + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, + presetId=preset_id, + loginBonusId=login_bonus_id, + loginBonusName=login_bonus_name, + presentId=present_id, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + conflict = sql.on_duplicate_key_update( + loginBonusName=login_bonus_name, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus( + self, version: int, preset_id: int, + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + ) + ).order_by(login_bonus.c.needLoginDayCount.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus_by_required_days( + self, version: int, preset_id: int, need_login_day_count: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + login_bonus.c.needLoginDayCount == need_login_day_count, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_login_bonus_preset( + self, version: int, preset_id: int, preset_name: str, is_enabled: bool + ) -> Optional[int]: + sql = insert(login_bonus_preset).values( + id=preset_id, + version=version, + presetName=preset_name, + isEnabled=is_enabled, + ) + + conflict = sql.on_duplicate_key_update( + presetName=preset_name, isEnabled=is_enabled + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus_presets( + self, version: int, is_enabled: bool = True + ) -> Optional[List[Row]]: + sql = login_bonus_preset.select( + and_( + login_bonus_preset.c.version == version, + login_bonus_preset.c.isEnabled == is_enabled, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: From 571a691e0eebae59a5f46494a697fe58ac4a11f1 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:54:27 +0200 Subject: [PATCH 05/10] chuni: added `use_login_bonus` check to UserLoginBonusApi --- titles/chuni/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 95c35e8..d41d415 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -396,7 +396,8 @@ class ChuniBase: def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) - if user_login_bonus is None: + # ignore the loginBonus request if its disabled in config + if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} user_login_list = [] From a60d52b7422b639ca982e63b07c0e8b5508bef2e Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 30 Mar 2023 22:58:45 +0200 Subject: [PATCH 06/10] chuni: fixed missing login boni IndexError --- titles/chuni/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d41d415..0eaabff 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -69,7 +69,13 @@ class ChuniBase: self.version, preset["id"] ) - # assume its not None + # skip the current bonus preset if no boni were found + if all_login_boni is None or len(all_login_boni) < 1: + self.logger.warn( + f"No bonus entries found for bonus preset {preset['id']}" + ) + continue + max_needed_days = all_login_boni[0]["needLoginDayCount"] # make sure to not show login boni after all days got redeemed From 0f642629a29e52ffa34d7d2bfaba5c14ab6c2907 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 01:53:36 -0400 Subject: [PATCH 07/10] add debug logger for the uri --- index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/index.py b/index.py index 13d826d..be48ac4 100644 --- a/index.py +++ b/index.py @@ -95,6 +95,7 @@ class HttpDispatcher(resource.Resource): ) def render_GET(self, request: Request) -> bytes: + self.logger.debug(request.uri) test = self.map_get.match(request.uri.decode()) client_ip = Utils.get_ip_addr(request) From de5f61b0de76cdd6f312cbebbeac0d691d613d92 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 03:35:14 -0400 Subject: [PATCH 08/10] pokken: add database tables --- titles/pokken/database.py | 6 + titles/pokken/schema/__init__.py | 4 + titles/pokken/schema/item.py | 27 +++++ titles/pokken/schema/match.py | 45 ++++++++ titles/pokken/schema/profile.py | 192 +++++++++++++++++++++++++++++++ titles/pokken/schema/static.py | 12 ++ 6 files changed, 286 insertions(+) create mode 100644 titles/pokken/schema/__init__.py create mode 100644 titles/pokken/schema/item.py create mode 100644 titles/pokken/schema/match.py create mode 100644 titles/pokken/schema/profile.py create mode 100644 titles/pokken/schema/static.py diff --git a/titles/pokken/database.py b/titles/pokken/database.py index f77f172..c940d83 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -1,7 +1,13 @@ from core.data import Data from core.config import CoreConfig +from .schema import * class PokkenData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) + + self.profile = PokkenProfileData(cfg, self.session) + self.match = PokkenMatchData(cfg, self.session) + self.item = PokkenItemData(cfg, self.session) + self.static = PokkenStaticData(cfg, self.session) diff --git a/titles/pokken/schema/__init__.py b/titles/pokken/schema/__init__.py new file mode 100644 index 0000000..81b8132 --- /dev/null +++ b/titles/pokken/schema/__init__.py @@ -0,0 +1,4 @@ +from .profile import PokkenProfileData +from .match import PokkenMatchData +from .item import PokkenItemData +from .static import PokkenStaticData diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py new file mode 100644 index 0000000..686b32f --- /dev/null +++ b/titles/pokken/schema/item.py @@ -0,0 +1,27 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +item = Table( + 'pokken_item', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column('category', Integer), + Column('content', Integer), + Column('type', Integer), + UniqueConstraint('user', 'category', 'content', 'type', name='pokken_item_uk'), + mysql_charset="utf8mb4", +) + +class PokkenItemData(BaseData): + """ + Items obtained as rewards + """ + pass diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py new file mode 100644 index 0000000..afe61f7 --- /dev/null +++ b/titles/pokken/schema/match.py @@ -0,0 +1,45 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +# Pokken sends depressingly little match data... +match_data = Table( + 'pokken_match_data', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('play_mode', Integer), + Column('result', Integer), + Column('ex_ko_num', Integer), + Column('wko_num', Integer), + Column('timeup_win_num', Integer), + Column('cool_ko_num', Integer), + Column('perfect_ko_num', Integer), + Column('use_navi', Integer), + Column('use_navi_cloth', Integer), + Column('use_aid_skill', Integer), + Column('play_date', TIMESTAMP), + mysql_charset="utf8mb4", +) + +class PokkenMatchData(BaseData): + """ + Match logs + """ + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: + pass + + def get_match(self, match_id: int) -> Optional[Row]: + pass + + def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]: + pass + + def get_matches(self, limit: int = 20) -> Optional[List[Row]]: + pass \ No newline at end of file diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py new file mode 100644 index 0000000..ed2471b --- /dev/null +++ b/titles/pokken/schema/profile.py @@ -0,0 +1,192 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me +# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue +profile = Table( + 'pokken_profile', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column('trainer_name', String(16)), # optional + Column('home_region_code', Integer), + Column('home_loc_name', String(255)), + Column('pref_code', Integer), + Column('navi_newbie_flag', Boolean), + Column('navi_enable_flag', Boolean), + Column('pad_vibrate_flag', Boolean), + Column('trainer_rank_point', Integer), + Column('wallet', Integer), + Column('fight_money', Integer), + Column('score_point', Integer), + Column('grade_max_num', Integer), + Column('extra_counter', Integer), # Optional + Column('total_play_days', Integer), + Column('play_date_time', Integer), + Column('lucky_box_fail_num', Integer), + Column('event_reward_get_flag', Integer), + Column('rank_pvp_all', Integer), + Column('rank_pvp_loc', Integer), + Column('rank_cpu_all', Integer), + Column('rank_cpu_loc', Integer), + Column('rank_event', Integer), + Column('awake_num', Integer), + Column('use_support_num', Integer), + Column('rankmatch_flag', Integer), + Column('rankmatch_max', Integer), # Optional + Column('rankmatch_success', Integer), # Optional + Column('beat_num', Integer), # Optional + Column('title_text_id', Integer), + Column('title_plate_id', Integer), + Column('title_decoration_id', Integer), + Column('support_pokemon_list', JSON), # Repeated, Integer + Column('support_set_1', JSON), # Repeated, Integer + Column('support_set_2', JSON), # Repeated, Integer + Column('support_set_3', JSON), # Repeated, Integer + Column('navi_trainer', Integer), + Column('navi_version_id', Integer), + Column('aid_skill_list', JSON), # Repeated, Integer + Column('aid_skill', Integer), + Column('comment_text_id', Integer), + Column('comment_word_id', Integer), + Column('latest_use_pokemon', Integer), + Column('ex_ko_num', Integer), + Column('wko_num', Integer), + Column('timeup_win_num', Integer), + Column('cool_ko_num', Integer), + Column('cool_ko_num', Integer), + Column('perfect_ko_num', Integer), + Column('record_flag', Integer), + Column('continue_num', Integer), + Column('avatar_body', Integer), # Optional + Column('avatar_gender', Integer), # Optional + Column('avatar_background', Integer), # Optional + Column('avatar_head', Integer), # Optional + Column('avatar_battleglass', Integer), # Optional + Column('avatar_face0', Integer), # Optional + Column('avatar_face1', Integer), # Optional + Column('avatar_face2', Integer), # Optional + Column('avatar_bodyall', Integer), # Optional + Column('avatar_wear', Integer), # Optional + Column('avatar_accessory', Integer), # Optional + Column('avatar_stamp', Integer), # Optional + Column('event_state', Integer), + Column('event_id', Integer), + Column('sp_bonus_category_id_1', Integer), + Column('sp_bonus_key_value_1', Integer), + Column('sp_bonus_category_id_2', Integer), + Column('sp_bonus_key_value_2', Integer), + Column('last_play_event_id', Integer), # Optional + mysql_charset="utf8mb4", +) + +tutorial_progress = Table( + 'pokken_tutorial_progress', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_tutorial_progress_uk'), + mysql_charset="utf8mb4", +) + +rankmatch_progress = Table( + 'pokken_rankmatch_progress', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('progress', Integer), + UniqueConstraint('user', 'progress', name='pokken_rankmatch_progress_uk'), + mysql_charset="utf8mb4", +) + +achievement_flag = Table( + 'pokken_achievement_flag', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_achievement_flag_uk'), + mysql_charset="utf8mb4", +) + +event_achievement_flag = Table( + 'pokken_event_achievement_flag', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('flag', Integer), + UniqueConstraint('user', 'flag', name='pokken_event_achievement_flag_uk'), + mysql_charset="utf8mb4", +) + +event_achievement_param = Table( + 'pokken_event_achievement_param', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('param', Integer), + UniqueConstraint('user', 'param', name='pokken_event_achievement_param_uk'), + mysql_charset="utf8mb4", +) + +pokemon_data = Table( + 'pokken_pokemon_data', + metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column('char_id', Integer, nullable=False), + Column('illustration_book_no', Integer, nullable=False), + Column('pokemon_exp', Integer, nullable=False), + Column('battle_num_vs_wan', Integer, nullable=False), + Column('win_vs_wan', Integer, nullable=False), + Column('battle_num_vs_lan', Integer, nullable=False), + Column('win_vs_lan', Integer, nullable=False), + Column('battle_num_vs_cpu', Integer, nullable=False), + Column('win_cpu', Integer, nullable=False), + Column('battle_all_num_tutorial', Integer, nullable=False), + Column('battle_num_tutorial', Integer, nullable=False), + Column('bp_point_atk', Integer, nullable=False), + Column('bp_point_res', Integer, nullable=False), + Column('bp_point_def', Integer, nullable=False), + Column('bp_point_sp', Integer, nullable=False), +) + +class PokkenProfileData(BaseData): + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user = user_id) + conflict = sql.on_duplicate_key_update(user = user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create pokken profile for user {user_id}!") + return None + return result.lastrowid + + def set_profile_name(self, user_id: int, new_name: str) -> None: + sql = update(profile).where(profile.c.user == user_id).values(trainer_name = new_name) + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to update pokken profile name for user {user_id}!") + + def update_profile(self, user_id: int, profile_data: Dict) -> None: + """ + TODO: Find out what comes in on the SaveUserRequestData protobuf and save it! + """ + pass + + def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: + pass + + def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: + pass + + def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: + pass diff --git a/titles/pokken/schema/static.py b/titles/pokken/schema/static.py new file mode 100644 index 0000000..63aa2dd --- /dev/null +++ b/titles/pokken/schema/static.py @@ -0,0 +1,12 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +class PokkenStaticData(BaseData): + pass \ No newline at end of file From 5ec280ab8cb232893f6e3e68a36e273225a6a96d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 04:42:40 -0400 Subject: [PATCH 09/10] pokken: fill LoadUser, add auto_register flag --- example_config/pokken.yaml | 3 +- titles/pokken/base.py | 184 ++++++++++++++++++++++---------- titles/pokken/config.py | 10 ++ titles/pokken/schema/profile.py | 6 ++ 4 files changed, 143 insertions(+), 60 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 7400060..225e980 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -5,4 +5,5 @@ server: port: 9000 port_stun: 9001 port_turn: 9002 - port_admission: 9003 \ No newline at end of file + port_admission: 9003 + auto_register: True \ No newline at end of file diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 1a9d5da..5fa200b 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -5,8 +5,9 @@ import random from core.data import Data from core import CoreConfig -from titles.pokken.config import PokkenConfig -from titles.pokken.proto import jackal_pb2 +from .config import PokkenConfig +from .proto import jackal_pb2 +from .database import PokkenData class PokkenBase: @@ -15,7 +16,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") - self.data = Data(core_cfg) + self.data = PokkenData(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -98,7 +99,7 @@ class PokkenBase: res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS settings = jackal_pb2.LoadClientSettingsResponseData() - settings.money_magnification = 0 + settings.money_magnification = 1 settings.continue_bonus_exp = 100 settings.continue_fight_money = 100 settings.event_bonus_exp = 100 @@ -132,72 +133,130 @@ class PokkenBase: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_USER - access_code = request.load_user.access_code + access_code = request.load_user.access_code + load_usr = jackal_pb2.LoadUserResponseData() user_id = self.data.card.get_user_id_from_card(access_code) - if user_id is None: # TODO: Toggle auto-register + if user_id is None and self.game_cfg.server.auto_register: user_id = self.data.user.create_user() card_id = self.data.card.create_card(user_id, access_code) self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") - # TODO: Check for user data. For now just treat ever card-in as a new user - - load_usr = jackal_pb2.LoadUserResponseData() + elif user_id is None: + self.logger.info(f"Registration of card {access_code} blocked!") + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + """ + TODO: Add repeated values + tutorial_progress_flag + rankmatch_progress + support_pokemon_list + support_set_1 + support_set_2 + support_set_3 + aid_skill_list + achievement_flag + pokemon_data + event_achievement_flag + event_achievement_param + """ + profile = self.data.profile.get_profile(user_id) load_usr.commidserv_result = 1 load_usr.load_hash = 1 load_usr.cardlock_status = False load_usr.banapass_id = user_id - load_usr.access_code = access_code - load_usr.new_card_flag = True + load_usr.access_code = access_code load_usr.precedent_release_flag = 0xFFFFFFFF - load_usr.navi_newbie_flag = True - load_usr.navi_enable_flag = True - load_usr.pad_vibrate_flag = True - load_usr.home_region_code = 0 - load_usr.home_loc_name = "" - load_usr.pref_code = 0 - load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) - load_usr.trainer_rank_point = 0 - load_usr.wallet = 0 - load_usr.fight_money = 0 - load_usr.score_point = 0 - load_usr.grade_max_num = 0 - load_usr.total_play_days = 0 - load_usr.play_date_time = 0 - load_usr.lucky_box_fail_num = 0 - load_usr.event_reward_get_flag = 0 - load_usr.rank_pvp_all = 0 - load_usr.rank_pvp_loc = 0 - load_usr.rank_cpu_all = 0 - load_usr.rank_cpu_loc = 0 - load_usr.rank_event = 0 - load_usr.awake_num = 0 - load_usr.use_support_num = 0 - load_usr.rankmatch_flag = 0 - load_usr.title_text_id = 0 - load_usr.title_plate_id = 0 - load_usr.title_decoration_id = 0 - load_usr.navi_trainer = 0 - load_usr.navi_version_id = 0 - load_usr.aid_skill = 0 - load_usr.comment_text_id = 0 - load_usr.comment_word_id = 0 - load_usr.latest_use_pokemon = 0 - load_usr.ex_ko_num = 0 - load_usr.wko_num = 0 - load_usr.timeup_win_num = 0 - load_usr.cool_ko_num = 0 - load_usr.perfect_ko_num = 0 - load_usr.record_flag = 0 - load_usr.site_register_status = 0 - load_usr.continue_num = 0 - load_usr.event_state = 0 - load_usr.event_id = 0 - load_usr.sp_bonus_category_id_1 = 0 - load_usr.sp_bonus_key_value_1 = 0 - load_usr.sp_bonus_category_id_2 = 0 - load_usr.sp_bonus_key_value_2 = 0 + + if profile is None: + profile_id = self.data.profile.create_profile(user_id) + profile_dict = {'id': profile_id, 'user': user_id} + pokemon_data = [] + tutorial_progress = [] + rankmatch_progress = [] + achievement_flag = [] + event_achievement_flag = [] + event_achievement_param = [] + load_usr.new_card_flag = True + + else: + profile_dict = profile._asdict() + pokemon_data = self.data.profile.get_all_pokemon_data(user_id) + tutorial_progress = [] + rankmatch_progress = [] + achievement_flag = [] + event_achievement_flag = [] + event_achievement_param = [] + load_usr.new_card_flag = False + + load_usr.navi_newbie_flag = profile_dict.get('navi_newbie_flag', True) + load_usr.navi_enable_flag = profile_dict.get('navi_enable_flag', True) + load_usr.pad_vibrate_flag = profile_dict.get('pad_vibrate_flag', True) + load_usr.home_region_code = profile_dict.get('home_region_code', 0) + load_usr.home_loc_name = profile_dict.get('home_loc_name', "") + load_usr.pref_code = profile_dict.get('pref_code', 0) + load_usr.trainer_name = profile_dict.get('trainer_name', "Newb" + str(random.randint(1111,999999))) + load_usr.trainer_rank_point = profile_dict.get('trainer_rank_point', 0) + load_usr.wallet = profile_dict.get('wallet', 0) + load_usr.fight_money = profile_dict.get('fight_money', 0) + load_usr.score_point = profile_dict.get('score_point', 0) + load_usr.grade_max_num = profile_dict.get('grade_max_num', 0) + load_usr.extra_counter = profile_dict.get('extra_counter', 0) + load_usr.total_play_days = profile_dict.get('total_play_days', 0) + load_usr.play_date_time = profile_dict.get('play_date_time', 0) + load_usr.lucky_box_fail_num = profile_dict.get('lucky_box_fail_num', 0) + load_usr.event_reward_get_flag = profile_dict.get('event_reward_get_flag', 0) + load_usr.rank_pvp_all = profile_dict.get('rank_pvp_all', 0) + load_usr.rank_pvp_loc = profile_dict.get('rank_pvp_loc', 0) + load_usr.rank_cpu_all = profile_dict.get('rank_cpu_all', 0) + load_usr.rank_cpu_loc = profile_dict.get('rank_cpu_loc', 0) + load_usr.rank_event = profile_dict.get('rank_event', 0) + load_usr.awake_num = profile_dict.get('awake_num', 0) + load_usr.use_support_num = profile_dict.get('use_support_num', 0) + load_usr.rankmatch_flag = profile_dict.get('rankmatch_flag', 0) + load_usr.rankmatch_max = profile_dict.get('rankmatch_max', 0) + load_usr.rankmatch_success = profile_dict.get('rankmatch_success', 0) + load_usr.beat_num = profile_dict.get('beat_num', 0) + load_usr.title_text_id = profile_dict.get('title_text_id', 0) + load_usr.title_plate_id = profile_dict.get('title_plate_id', 0) + load_usr.title_decoration_id = profile_dict.get('title_decoration_id', 0) + load_usr.navi_trainer = profile_dict.get('navi_trainer', 0) + load_usr.navi_version_id = profile_dict.get('navi_version_id', 0) + load_usr.aid_skill = profile_dict.get('aid_skill', 0) + load_usr.comment_text_id = profile_dict.get('comment_text_id', 0) + load_usr.comment_word_id = profile_dict.get('comment_word_id', 0) + load_usr.latest_use_pokemon = profile_dict.get('latest_use_pokemon', 0) + load_usr.ex_ko_num = profile_dict.get('ex_ko_num', 0) + load_usr.wko_num = profile_dict.get('wko_num', 0) + load_usr.timeup_win_num = profile_dict.get('timeup_win_num', 0) + load_usr.cool_ko_num = profile_dict.get('cool_ko_num', 0) + load_usr.perfect_ko_num = profile_dict.get('perfect_ko_num', 0) + load_usr.record_flag = profile_dict.get('record_flag', 0) + load_usr.site_register_status = profile_dict.get('site_register_status', 0) + load_usr.continue_num = profile_dict.get('continue_num', 0) + + load_usr.avatar_body = profile_dict.get('avatar_body', 0) + load_usr.avatar_gender = profile_dict.get('avatar_gender', 0) + load_usr.avatar_background = profile_dict.get('avatar_background', 0) + load_usr.avatar_head = profile_dict.get('avatar_head', 0) + load_usr.avatar_battleglass = profile_dict.get('avatar_battleglass', 0) + load_usr.avatar_face0 = profile_dict.get('avatar_face0', 0) + load_usr.avatar_face1 = profile_dict.get('avatar_face1', 0) + load_usr.avatar_face2 = profile_dict.get('avatar_face2', 0) + load_usr.avatar_bodyall = profile_dict.get('avatar_bodyall', 0) + load_usr.avatar_wear = profile_dict.get('avatar_wear', 0) + load_usr.avatar_accessory = profile_dict.get('avatar_accessory', 0) + load_usr.avatar_stamp = profile_dict.get('avatar_stamp', 0) + + load_usr.event_state = profile_dict.get('event_state', 0) + load_usr.event_id = profile_dict.get('event_id', 0) + load_usr.sp_bonus_category_id_1 = profile_dict.get('sp_bonus_category_id_1', 0) + load_usr.sp_bonus_key_value_1 = profile_dict.get('sp_bonus_key_value_1', 0) + load_usr.sp_bonus_category_id_2 = profile_dict.get('sp_bonus_category_id_2', 0) + load_usr.sp_bonus_key_value_2 = profile_dict.get('sp_bonus_key_value_2', 0) + load_usr.last_play_event_id = profile_dict.get('last_play_event_id', 0) res.load_user.CopyFrom(load_usr) return res.SerializeToString() @@ -208,10 +267,17 @@ class PokkenBase: res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK return res.SerializeToString() + def handle_save_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_USER + + return res.SerializeToString() + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 - res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG return res.SerializeToString() def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: diff --git a/titles/pokken/config.py b/titles/pokken/config.py index b53fc86..e25a8c7 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -49,6 +49,16 @@ class PokkenServerConfig: self.__config, "pokken", "server", "port_admission", default=9003 ) + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with pokken + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "auto_register", default=True + ) + class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index ed2471b..da6736c 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -182,6 +182,12 @@ class PokkenProfileData(BaseData): """ pass + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: return None + return result.fetchone() + def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]: pass From bf6c7d39f5255337ce7fdb4931874525a3d39637 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 10 Apr 2023 04:47:19 -0400 Subject: [PATCH 10/10] pokken: small cleanup on LoadUser --- titles/pokken/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 5fa200b..96d5efd 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -182,7 +182,8 @@ class PokkenBase: load_usr.new_card_flag = True else: - profile_dict = profile._asdict() + profile_dict = { k: v for k, v in profile._asdict().items() if v is not None } + self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})") pokemon_data = self.data.profile.get_all_pokemon_data(user_id) tutorial_progress = [] rankmatch_progress = []