diff --git a/core/config.py b/core/config.py index 83a941a..14f06f5 100644 --- a/core/config.py +++ b/core/config.py @@ -150,6 +150,12 @@ class DatabaseConfig: default=10000, ) + @property + def enable_memcached(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "database", "enable_memcached", default=True + ) + @property def memcached_host(self) -> str: return CoreConfig.get_config_field( diff --git a/core/data/cache.py b/core/data/cache.py index cabf597..1490826 100644 --- a/core/data/cache.py +++ b/core/data/cache.py @@ -17,7 +17,7 @@ except ModuleNotFoundError: def cached(lifetime: int = 10, extra_key: Any = None) -> Callable: def _cached(func: Callable) -> Callable: - if has_mc: + if has_mc and (cfg and cfg.database.enable_memcached): hostname = "127.0.0.1" if cfg: hostname = cfg.database.memcached_host diff --git a/core/data/schema/versions/SBZV_5_rollback.sql b/core/data/schema/versions/SBZV_5_rollback.sql new file mode 100644 index 0000000..851d357 --- /dev/null +++ b/core/data/schema/versions/SBZV_5_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile + DROP skn_eqp; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_6_upgrade.sql b/core/data/schema/versions/SBZV_6_upgrade.sql new file mode 100644 index 0000000..d417506 --- /dev/null +++ b/core/data/schema/versions/SBZV_6_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile + ADD skn_eqp INT NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 8f52807..ac27f55 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -59,6 +59,28 @@ python read.py --game SDBT --version --binfolder /path/to/game/fold The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories. +### Config + +Config file is located in `config/chuni.yaml`. + +| Option | Info | +|------------------|----------------------------------------------------------------------------------------------------------------| +| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) | +| `name` | If this is set, all players that are not on a team will use this one by default. | +| `rank_scale` | Scales the in-game ranking based on the number of teams within the database | +| `use_login_bonus`| This is used to enable the login bonuses | +| `crypto` | This option is used to enable the TLS Encryption | + + +**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** + +```yaml +crypto: + encrypted_only: False + keys: + 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] +``` + ### Database upgrade Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see @@ -88,6 +110,36 @@ After a failed Online Battle the room will be deleted. The host is used for the - Timer countdown should be handled globally and not by one user - Game can freeze or can crash if someone (especially the host) leaves the matchmaking +### Rivals + +You can configure up to 4 rivals in Chunithm on a per-user basis. There is no UI to do this currently, so in the database, you can do this: +```sql +INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (, , , 2); +INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (, , , 2); +``` +Note that the version **must match**, otherwise song lookup may not work. + +### Teams + +You can also configure teams for users to be on. There is no UI to do this currently, so in the database, you can do this: +```sql +INSERT INTO aime.chuni_profile_team (teamName) VALUES (); +``` +Team names can be regular ASCII, and they will be displayed ingame. + +On smaller installations, you may also wish to enable scaled team rankings. By default, Chunithm determines team ranking within the first 100 teams. This can be configured in the YAML: +```yaml +team: + rank_scale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses. +``` + +### Favorite songs +You can set the songs that will be in a user's Favorite Songs category using the following SQL entries: +```sql +INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (, , , 1); +``` +The songId is based on the actual ID within your version of Chunithm. + ## crossbeats REV. @@ -211,6 +263,9 @@ Config file is located in `config/diva.yaml`. | `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | | `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | +### Custom PV Lists (databanks) + +In order to use custom PV Lists, simply drop in your .dat files inside of /titles/diva/data/ and make sure they are called PvList0.dat, PvList1.dat, PvList2.dat, PvList3.dat and PvList4.dat exactly. ### Database upgrade @@ -257,9 +312,19 @@ Config file is located in `config/ongeki.yaml`. | Option | Info | |------------------|----------------------------------------------------------------------------------------------------------------| | `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them | +| `crypto` | This option is used to enable the TLS Encryption | Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions. +**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** + +```yaml +crypto: + encrypted_only: False + keys: + 7: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] +``` + ### Database upgrade Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see @@ -509,6 +574,8 @@ python dbutils.py --game SDEW upgrade - Player title is currently static and cannot be changed in-game - QR Card Scanning currently only load a static hero +**Network hashing in GssSite.dll must be disabled** + ### Credits for SAO support: - Midorica - Limited Network Support diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 59db51e..09e3569 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -1,9 +1,11 @@ server: enable: True loglevel: "info" + news_msg: "" team: - name: ARTEMiS + name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. + rank_scale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses. mods: use_login_bonus: True diff --git a/example_config/core.yaml b/example_config/core.yaml index 1ecb7ff..76ec5b1 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -24,6 +24,7 @@ database: sha2_password: False loglevel: "warn" user_table_autoincrement_start: 10000 + enable_memcached: True memcached_host: "localhost" frontend: diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 0b468d0..21a6719 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -181,7 +181,17 @@ class ChuniBase: return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_message_api_request(self, data: Dict) -> Dict: - return {"type": data["type"], "length": "0", "gameMessageList": []} + return { + "type": data["type"], + "length": 1, + "gameMessageList": [{ + "id": 1, + "type": 1, + "message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_cfg.server.news_msg else self.game_cfg.server.news_msg, + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0" + }] + } def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "gameRankingList": []} @@ -361,11 +371,98 @@ class ChuniBase: "userDuelList": duel_list, } + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_rival(data["rivalId"]) + if p is None: + return {} + userRivalData = { + "rivalId": p.user, + "rivalName": p.userName + } + return { + "userId": data["userId"], + "userRivalData": userRivalData + } + + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) + if m is None: + return {} + + user_rival_music_list = [] + for music in m: + actual_music_id = self.data.static.get_song(music["musicId"]) + if actual_music_id is None: + music_id = music["musicId"] + else: + music_id = actual_music_id["songId"] + level = music["level"] + score = music["score"] + rank = music["rank"] + + # Find the existing entry for the current musicId in the user_rival_music_list + music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None) + + if music_entry is None: + # If the entry doesn't exist, create a new entry + music_entry = { + "musicId": music_id, + "length": 0, + "userRivalMusicDetailList": [] + } + user_rival_music_list.append(music_entry) + + # Check if the current score is higher than the previous highest score for the level + level_entry = next( + ( + entry + for entry in music_entry["userRivalMusicDetailList"] + if entry["level"] == level + ), + None, + ) + if level_entry is None or score > level_entry["scoreMax"]: + # If the level entry doesn't exist or the score is higher, update or add the entry + level_entry = { + "level": level, + "scoreMax": score, + "scoreRank": rank + } + + if level_entry not in music_entry["userRivalMusicDetailList"]: + music_entry["userRivalMusicDetailList"].append(level_entry) + + music_entry["length"] = len(music_entry["userRivalMusicDetailList"]) + + result = { + "userId": data["userId"], + "rivalId": data["rivalId"], + "nextIndex": -1, + "userRivalMusicList": user_rival_music_list + } + + return result + def handle_get_user_rival_music_api_requestded(self, data: Dict) -> Dict: + m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) + if m is None: + return {} + + userRivalMusicList = [] + for music in m: + self.logger.debug(music["point"]) + + return { + "userId": data["userId"], + "rivalId": data["rivalId"], + "nextIndex": -1 + + } + def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] # still needs to be implemented on WebUI - # 1: Music, 3: Character + # 1: Music, 2: User, 3: Character fav_list = self.data.item.get_all_favorites( data["userId"], self.version, fav_kind=int(data["kind"]) ) @@ -501,7 +598,7 @@ class ChuniBase: if len(song_list) >= max_ct: break - if len(song_list) >= next_idx + max_ct: + if len(music_detail) >= next_idx + max_ct: next_idx += max_ct else: next_idx = -1 @@ -600,25 +697,43 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: use the database "chuni_profile_team" with a GUI + # Default values + team_id = 65535 team_name = self.game_cfg.team.team_name - if team_name == "": + team_rank = 0 + + # Get user profile + profile = self.data.profile.get_profile_data(data["userId"], self.version) + if profile and profile["teamId"]: + # Get team by id + team = self.data.profile.get_team_by_id(profile["teamId"]) + + if team: + team_id = team["id"] + team_name = team["teamName"] + # Determine whether to use scaled ranks, or original system + if self.game_cfg.team.rank_scale: + team_rank = self.data.profile.get_team_rank(team["id"]) + else: + team_rank = self.data.profile.get_team_rank_actual(team["id"]) + + # Don't return anything if no team name has been defined for defaults and there is no team set for the player + if not profile["teamId"] and team_name == "": return {"userId": data["userId"], "teamId": 0} return { "userId": data["userId"], - "teamId": 1, - "teamRank": 1, + "teamId": team_id, + "teamRank": team_rank, "teamName": team_name, "userTeamPoint": { "userId": data["userId"], - "teamId": 1, + "teamId": team_id, "orderId": 1, "teamPoint": 1, "aggrDate": data["playDate"], }, } - def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], @@ -709,9 +824,25 @@ class ChuniBase: self.data.score.put_playlog(user_id, playlog) if "userTeamPoint" in upsert: - # TODO: team stuff - pass + team_points = upsert["userTeamPoint"] + try: + for tp in team_points: + if tp["teamId"] != '65535': + # Fetch the current team data + current_team = self.data.profile.get_team_by_id(tp["teamId"]) + # Calculate the new teamPoint + new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"] + + # Prepare the data to update + team_data = { + "teamPoint": new_team_point + } + + # Update the team data + self.data.profile.update_team(tp["teamId"], team_data) + except: + pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: self.data.item.put_map_area(user_id, map_area) @@ -757,4 +888,4 @@ class ChuniBase: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } + } \ No newline at end of file diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 48d70d2..05cc8ad 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -19,6 +19,12 @@ class ChuniServerConfig: self.__config, "chuni", "server", "loglevel", default="info" ) ) + + @property + def news_msg(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "server", "news_msg", default="" + ) class ChuniTeamConfig: @@ -30,6 +36,11 @@ class ChuniTeamConfig: return CoreConfig.get_config_field( self.__config, "chuni", "team", "name", default="" ) + @property + def rank_scale(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "rank_scale", default="False" + ) class ChuniModsConfig: diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 0268621..fa7a4ab 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -637,3 +637,103 @@ class ChuniProfileData(BaseData): if result is None: return None return result.fetchall() + + def get_team_by_id(self, team_id: int) -> Optional[Row]: + sql = select(team).where(team.c.id == team_id) + result = self.execute(sql) + + if result is None: + return None + return result.fetchone() + + def get_team_rank_actual(self, team_id: int) -> int: + # Normal ranking system, likely the one used in the real servers + # Query all teams sorted by 'teamPoint' + result = self.execute( + select(team.c.id).order_by(team.c.teamPoint.desc()) + ) + + # Get the rank of the team with the given team_id + rank = None + for i, row in enumerate(result, start=1): + if row.id == team_id: + rank = i + break + + # Return the rank if found, or a default rank otherwise + return rank if rank is not None else 0 + + def get_team_rank(self, team_id: int) -> int: + # Scaled ranking system, designed for smaller instances. + # Query all teams sorted by 'teamPoint' + result = self.execute( + select(team.c.id).order_by(team.c.teamPoint.desc()) + ) + + # Count total number of teams + total_teams = self.execute(select(func.count()).select_from(team)).scalar() + + # Get the rank of the team with the given team_id + rank = None + for i, row in enumerate(result, start=1): + if row.id == team_id: + rank = i + break + + # If the team is not found, return default rank + if rank is None: + return 0 + + # Define rank tiers + tiers = { + 1: range(1, int(total_teams * 0.1) + 1), # Rainbow + 2: range(int(total_teams * 0.1) + 1, int(total_teams * 0.4) + 1), # Gold + 3: range(int(total_teams * 0.4) + 1, int(total_teams * 0.7) + 1), # Silver + 4: range(int(total_teams * 0.7) + 1, total_teams + 1), # Grey + } + + # Assign rank based on tier + for tier_rank, tier_range in tiers.items(): + if rank in tier_range: + return tier_rank + + # Return default rank if not found in any tier + return 0 + + def update_team(self, team_id: int, team_data: Dict) -> bool: + team_data["id"] = team_id + + sql = insert(team).values(**team_data) + conflict = sql.on_duplicate_key_update(**team_data) + + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"update_team: Failed to update team! team id: {team_id}" + ) + return False + return True + def get_rival(self, rival_id: int) -> Optional[Row]: + sql = select(profile).where(profile.c.user == rival_id) + result = self.execute(sql) + + if result is None: + return None + return result.fetchone() + def get_overview(self) -> Dict: + # Fetch and add up all the playcounts + playcount_sql = self.execute(select(profile.c.playCount)) + + if playcount_sql is None: + self.logger.warn( + f"get_overview: Couldn't pull playcounts" + ) + return 0 + + total_play_count = 0; + for row in playcount_sql: + total_play_count += row[0] + return { + "total_play_count": total_play_count + } \ No newline at end of file diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 203aa11..ab26f5f 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -200,3 +200,10 @@ class ChuniScoreData(BaseData): if result is None: return None return result.lastrowid + + def get_rival_music(self, rival_id: int, index: int, max_count: int) -> Optional[List[Dict]]: + sql = select(playlog).where(playlog.c.user == rival_id).limit(max_count).offset(index) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() \ No newline at end of file diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 1f6625a..fe32d41 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -453,6 +453,15 @@ class ChuniStaticData(BaseData): return None return result.fetchone() + def get_song(self, music_id: int) -> Optional[Row]: + sql = music.select(music.c.id == music_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_avatar( self, version: int, @@ -587,4 +596,4 @@ class ChuniStaticData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchone() \ No newline at end of file diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index 46ea090..9a9e6ef 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -7,4 +7,4 @@ index = DivaServlet database = DivaData reader = DivaReader game_codes = [DivaConstants.GAME_CODE] -current_schema_version = 5 +current_schema_version = 6 diff --git a/titles/diva/base.py b/titles/diva/base.py index 3f9ef12..6db0dbc 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -402,7 +402,7 @@ class DivaBase: response += f"&lv_num={profile['lv_num']}" response += f"&lv_pnt={profile['lv_pnt']}" response += f"&vcld_pts={profile['vcld_pts']}" - response += f"&skn_eqp={profile['use_pv_skn_eqp']}" + response += f"&skn_eqp={profile['skn_eqp']}" response += f"&btn_se_eqp={profile['btn_se_eqp']}" response += f"&sld_se_eqp={profile['sld_se_eqp']}" response += f"&chn_sld_se_eqp={profile['chn_sld_se_eqp']}" diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 7107068..7bd6bf0 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -51,6 +51,7 @@ profile = Table( Column("rgo_sts", Integer, nullable=False, server_default="1"), Column("lv_efct_id", Integer, nullable=False, server_default="0"), Column("lv_plt_id", Integer, nullable=False, server_default="1"), + Column("skn_eqp", Integer, nullable=False, server_default="0"), Column("passwd_stat", Integer, nullable=False, server_default="0"), Column("passwd", String(12), nullable=False, server_default="**********"), Column( diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 0b849b3..9663e81 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -157,7 +157,6 @@ class PokkenBase: support_set_3 aid_skill_list achievement_flag - pokemon_data event_achievement_flag event_achievement_param """ @@ -200,7 +199,7 @@ class PokkenBase: 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)) + "trainer_name", f"Newb{str(user_id).zfill(4)}" ) load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) load_usr.wallet = profile_dict.get("wallet", 0) @@ -262,6 +261,29 @@ class PokkenBase: 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) + if pokemon_data is not None: + for pkmn in pokemon_data: + pkmn_d = pkmn._asdict() + pkm = jackal_pb2.LoadUserResponseData.PokemonData() + + pkm.char_id = pkmn_d.get('char_id', 0) + pkm.illustration_book_no = pkmn_d.get('illustration_book_no', 0) + pkm.pokemon_exp = pkmn_d.get('pokemon_exp', 0) + pkm.battle_num_vs_wan = pkmn_d.get('battle_num_vs_wan', 0) + pkm.win_vs_wan = pkmn_d.get('win_vs_wan', 0) + pkm.battle_num_vs_lan = pkmn_d.get('battle_num_vs_lan', 0) + pkm.win_vs_lan = pkmn_d.get('win_vs_lan', 0) + pkm.battle_num_vs_cpu = pkmn_d.get('battle_num_vs_cpu', 0) + pkm.win_cpu = pkmn_d.get('win_cpu', 0) + pkm.battle_all_num_tutorial = pkmn_d.get('battle_all_num_tutorial', 0) + pkm.battle_num_tutorial = pkmn_d.get('battle_num_tutorial', 0) + pkm.bp_point_atk = pkmn_d.get('bp_point_atk', 0) + pkm.bp_point_res = pkmn_d.get('bp_point_res', 0) + pkm.bp_point_def = pkmn_d.get('bp_point_def', 0) + pkm.bp_point_sp = pkmn_d.get('bp_point_sp', 0) + + load_usr.pokemon_data.append(pkm) + res.load_user.CopyFrom(load_usr) return res.SerializeToString() @@ -325,7 +347,7 @@ class PokkenBase: for evt_param in req.event_achievement_param: evt_params.append(evt_param) - self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, ) + self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, req.last_play_event_id) for reward in req.reward_data: self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 3429f7f..ab81d77 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -3,6 +3,7 @@ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, an from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.sql.functions import coalesce from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert @@ -257,18 +258,27 @@ class PokkenProfileData(BaseData): user=user_id, char_id=pokemon_id, illustration_book_no=illust_no, - bp_point_atk=atk, - bp_point_res=res, - bp_point_def=defe, - bp_point_sp=sp, + pokemon_exp=0, + battle_num_vs_wan=0, + win_vs_wan=0, + battle_num_vs_lan=0, + win_vs_lan=0, + battle_num_vs_cpu=0, + win_cpu=0, + battle_all_num_tutorial=0, + battle_num_tutorial=0, + bp_point_atk=1+atk, + bp_point_res=1+res, + bp_point_def=1+defe, + bp_point_sp=1+sp, ) conflict = sql.on_duplicate_key_update( illustration_book_no=illust_no, - bp_point_atk=atk, - bp_point_res=res, - bp_point_def=defe, - bp_point_sp=sp, + bp_point_atk=pokemon_data.c.bp_point_atk + atk, + bp_point_res=pokemon_data.c.bp_point_res + res, + bp_point_def=pokemon_data.c.bp_point_def + defe, + bp_point_sp=pokemon_data.c.bp_point_sp + sp, ) result = self.execute(conflict) @@ -284,7 +294,7 @@ class PokkenProfileData(BaseData): xp: int ) -> None: sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( - pokemon_exp=pokemon_data.c.pokemon_exp + xp + pokemon_exp=coalesce(pokemon_data.c.pokemon_exp, 0) + xp ) result = self.execute(sql) @@ -292,29 +302,38 @@ class PokkenProfileData(BaseData): self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}") def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: - pass + sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id)) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: - pass + sql = pokemon_data.select(pokemon_data.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def put_pokemon_battle_result( self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT ) -> None: """ Records the match stats (type and win/loss) for the pokemon and profile + coalesce(pokemon_data.c.win_vs_wan, 0) """ sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( - battle_num_tutorial=pokemon_data.c.battle_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_num_tutorial, - battle_all_num_tutorial=pokemon_data.c.battle_all_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_all_num_tutorial, + battle_num_tutorial=coalesce(pokemon_data.c.battle_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_num_tutorial, 0), + battle_all_num_tutorial=coalesce(pokemon_data.c.battle_all_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_all_num_tutorial, 0), - battle_num_vs_cpu=pokemon_data.c.battle_num_vs_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else pokemon_data.c.battle_num_vs_cpu, - win_cpu=pokemon_data.c.win_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_cpu, + battle_num_vs_cpu=coalesce(pokemon_data.c.battle_num_vs_cpu, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else coalesce(pokemon_data.c.battle_num_vs_cpu, 0), + win_cpu=coalesce(pokemon_data.c.win_cpu, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_cpu, 0), - battle_num_vs_lan=pokemon_data.c.battle_num_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else pokemon_data.c.battle_num_vs_lan, - win_vs_lan=pokemon_data.c.win_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_lan, + battle_num_vs_lan=coalesce(pokemon_data.c.battle_num_vs_lan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else coalesce(pokemon_data.c.battle_num_vs_lan, 0), + win_vs_lan=coalesce(pokemon_data.c.win_vs_lan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_vs_lan, 0), - battle_num_vs_wan=pokemon_data.c.battle_num_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else pokemon_data.c.battle_num_vs_wan, - win_vs_wan=pokemon_data.c.win_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_wan, + battle_num_vs_wan=coalesce(pokemon_data.c.battle_num_vs_wan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else coalesce(pokemon_data.c.battle_num_vs_wan, 0), + win_vs_wan=coalesce(pokemon_data.c.win_vs_wan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_vs_wan, 0), ) result = self.execute(sql) @@ -335,11 +354,11 @@ class PokkenProfileData(BaseData): Records profile stats """ sql = update(profile).where(profile.c.user==user_id).values( - ex_ko_num=profile.c.ex_ko_num + exkos, - wko_num=profile.c.wko_num + wkos, - timeup_win_num=profile.c.timeup_win_num + timeout_wins, - cool_ko_num=profile.c.cool_ko_num + cool_kos, - perfect_ko_num=profile.c.perfect_ko_num + perfects, + ex_ko_num=coalesce(profile.c.ex_ko_num, 0) + exkos, + wko_num=coalesce(profile.c.wko_num, 0) + wkos, + timeup_win_num=coalesce(profile.c.timeup_win_num, 0) + timeout_wins, + cool_ko_num=coalesce(profile.c.cool_ko_num, 0) + cool_kos, + perfect_ko_num=coalesce(profile.c.perfect_ko_num, 0) + perfects, continue_num=continues, ) @@ -348,10 +367,6 @@ class PokkenProfileData(BaseData): self.logger.warning(f"Failed to update stats for user {user_id}") def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None: - if support1 == 4294967295: - support1 = None - if support2 == 4294967295: - support2 = None sql = update(profile).where(profile.c.user==user_id).values( support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1, support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,