From 75842b5a886b3f61fdf50c361894a9887af39618 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:12:34 +0000 Subject: [PATCH 01/22] Add team support, implement team upsert, add rivals --- titles/chuni/base.py | 141 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 10 deletions(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index ed8d0fb..b9c1ae8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -361,11 +361,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"]) ) @@ -600,25 +687,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 +814,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 +878,4 @@ class ChuniBase: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } + } \ No newline at end of file From c01d3f49f57b650485f82526a4ec12d8594efe3c Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:13:19 +0000 Subject: [PATCH 02/22] Added call for getting rival's music lists --- titles/chuni/schema/score.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 From 043ff1700810ea5c6c863c09db0fc40eed161b99 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:14:53 +0000 Subject: [PATCH 03/22] Add team support, rivals, and test function for getting playcounts --- titles/chuni/schema/profile.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index f8edc33..b055fcb 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 From b42e8ab76c8bb6bfdf671aa013e3afc034ea9ff9 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:16:11 +0000 Subject: [PATCH 04/22] Added function for pulling a song via the DB unique ID instead of the native song ID --- titles/chuni/schema/static.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 85d0397..3796232 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 From eecd3a829dcc029dcf6e472e68ecc5f8aaa91417 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:40:49 +0000 Subject: [PATCH 05/22] Added value for team rank scaling, and documented it a bit --- example_config/chuni.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 59db51e..ed0aca0 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -3,7 +3,8 @@ server: loglevel: "info" team: - name: ARTEMiS + name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. + rankScale: 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 From 1bc8648e357bc75a8782799e5c57b98041846b90 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:56:09 +0000 Subject: [PATCH 06/22] I knew I forgot something (fixed config) --- example_config/chuni.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index ed0aca0..687b195 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -4,7 +4,7 @@ server: team: name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. - rankScale: 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. + 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 From 3c7ceabf4e1ac8e07bd1dac8d932a2539bc57771 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 10:04:25 +0000 Subject: [PATCH 07/22] And again --- titles/chuni/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 48d70d2..9b294ad 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -30,6 +30,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: From 33e0288e5e5ff41e41854081405f07b25ea4f76e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 1 Oct 2023 22:38:56 -0400 Subject: [PATCH 08/22] pokken: pokemon data save/load --- titles/pokken/base.py | 26 ++++++++++++++++++++++++-- titles/pokken/schema/profile.py | 31 ++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 0b849b3..56d8412 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() diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 3429f7f..3605b0a 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -257,18 +257,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) @@ -295,7 +304,11 @@ class PokkenProfileData(BaseData): pass 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 From 3d13eb16985d25da0da0f9ee90a6beb4cbdfef02 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 1 Oct 2023 23:49:47 -0400 Subject: [PATCH 09/22] pokken: add coalesce --- titles/pokken/schema/profile.py | 40 +++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 3605b0a..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 @@ -293,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) @@ -301,7 +302,11 @@ 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]]: sql = pokemon_data.select(pokemon_data.c.user == user_id) @@ -315,19 +320,20 @@ class PokkenProfileData(BaseData): ) -> 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) @@ -348,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, ) @@ -361,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, From d641705273c5aeed888da5625c9452c3b0cfe65f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 2 Oct 2023 15:12:29 -0400 Subject: [PATCH 10/22] pokken: fix last_play_event_id --- titles/pokken/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 56d8412..9663e81 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -347,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) From 9681f86e33738d0c825055d85eb4922fc0c16ee0 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Thu, 5 Oct 2023 07:56:46 +0000 Subject: [PATCH 11/22] Chunithm SQL documentation Figured I would outline all of the neat SQL tricks you can do with this build, as well as properly document the features. --- docs/game_specific_info.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index d5d1eff..163bcbd 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -88,6 +88,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. From b5ccd6794015d22165ce240896ee6885f9596be5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 5 Oct 2023 22:16:50 -0400 Subject: [PATCH 12/22] db: add memcache toggle --- core/config.py | 6 ++++++ core/data/cache.py | 2 +- example_config/core.yaml | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) 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/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: From 06d95c8c5fa4881b28cdb66ac4d8597b0bd71fd6 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 7 Oct 2023 11:59:01 -0400 Subject: [PATCH 13/22] Adding details under the game specific info doc --- docs/game_specific_info.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index e7d4d96..0d94e56 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 | +|------------------|----------------------------------------------------------------------------------------------------------------| +| `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:** + +```shell +crypto: + encrypted_only: False + keys: + 12: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + 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 @@ -241,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 @@ -287,9 +312,20 @@ 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:** + +```shell +crypto: + encrypted_only: False + keys: + 6: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + 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 From 41fcadfd55b39b5e8e0dbb9749a9dc369ffaa8f4 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 7 Oct 2023 12:04:33 -0400 Subject: [PATCH 14/22] quick fix to markdown file --- docs/game_specific_info.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 0d94e56..07a7fdf 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -73,7 +73,7 @@ Config file is located in `config/chuni.yaml`. **If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** -```shell +```yaml crypto: encrypted_only: False keys: @@ -318,7 +318,7 @@ Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignore **If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** -```shell +```yaml crypto: encrypted_only: False keys: From 56ddd3b1ccc0131dde24b4c80acaab0d827b67c9 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 7 Oct 2023 12:07:14 -0400 Subject: [PATCH 15/22] fix yet again to the md file --- docs/game_specific_info.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 07a7fdf..12a8e86 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -77,7 +77,6 @@ Config file is located in `config/chuni.yaml`. crypto: encrypted_only: False keys: - 12: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] ``` @@ -322,7 +321,6 @@ Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignore crypto: encrypted_only: False keys: - 6: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] 7: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] ``` From 1996f3f356e78fd7747ce84f10f845d6b897bb0e Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 7 Oct 2023 12:11:24 -0400 Subject: [PATCH 16/22] fix for chunithm song loading after 600 scores --- titles/chuni/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 2a6385d..1a7f350 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -588,7 +588,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 From bfaadff9f6d73d4ab87c9d87ba55987f76e0e4d4 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 7 Oct 2023 12:31:08 -0400 Subject: [PATCH 17/22] pushing news support customization for chunithm --- docs/game_specific_info.md | 1 + example_config/chuni.yaml | 1 + titles/chuni/base.py | 12 +++++++++++- titles/chuni/config.py | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 12a8e86..722fdda 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -65,6 +65,7 @@ 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 | diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 687b195..09e3569 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + news_msg: "" team: name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 1a7f350..0783b55 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_config.server.news_msg else self.game_config.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": []} diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 9b294ad..2ec1368 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: + CoreConfig.get_config_field( + self.__config, "chuni", "server", "news_msg", default="" + ) class ChuniTeamConfig: From 7fc5544c1598de9da61e139de277044d2dcf9b41 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 8 Oct 2023 15:52:41 -0400 Subject: [PATCH 18/22] adding a SAO note to documentation --- docs/game_specific_info.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 722fdda..ac27f55 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -574,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 From bad106ceba393e106a4ec6eb98460f08d9b9938c Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Mon, 9 Oct 2023 06:35:14 +0000 Subject: [PATCH 19/22] Fixed typo with game_cfg/game_config This resulted in an exception on Plost and earlier, leading to games not actually working. --- titles/chuni/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 0783b55..21a6719 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -187,7 +187,7 @@ class ChuniBase: "gameMessageList": [{ "id": 1, "type": 1, - "message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_config.server.news_msg else self.game_config.server.news_msg, + "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" }] From 0a12e935931dd765b90e99a679fbe18f930ab4ef Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Mon, 9 Oct 2023 06:47:55 +0000 Subject: [PATCH 20/22] news_msg config option doesn't actually return anything. Fixed! --- titles/chuni/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 2ec1368..05cc8ad 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -22,7 +22,7 @@ class ChuniServerConfig: @property def news_msg(self) -> str: - CoreConfig.get_config_field( + return CoreConfig.get_config_field( self.__config, "chuni", "server", "news_msg", default="" ) From 6d592dcbc7b02e0dab03c7f385ddde9538a3c228 Mon Sep 17 00:00:00 2001 From: Midorica Date: Tue, 10 Oct 2023 18:24:54 -0400 Subject: [PATCH 21/22] fixing skin issue with diva profile --- core/data/schema/versions/SBZV_5_rollback.sql | 2 ++ core/data/schema/versions/SBZV_6_upgrade.sql | 2 ++ titles/diva/base.py | 2 +- titles/diva/schema/profile.py | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 core/data/schema/versions/SBZV_5_rollback.sql create mode 100644 core/data/schema/versions/SBZV_6_upgrade.sql 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..4eddc17 --- /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..63ff6b3 --- /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/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( From aa9d48ccc9bae133812efe6a619de819029b32a6 Mon Sep 17 00:00:00 2001 From: Midorica Date: Tue, 10 Oct 2023 18:37:18 -0400 Subject: [PATCH 22/22] fixed the diva profile again --- core/data/schema/versions/SBZV_5_rollback.sql | 2 +- core/data/schema/versions/SBZV_6_upgrade.sql | 2 +- titles/diva/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/data/schema/versions/SBZV_5_rollback.sql b/core/data/schema/versions/SBZV_5_rollback.sql index 4eddc17..851d357 100644 --- a/core/data/schema/versions/SBZV_5_rollback.sql +++ b/core/data/schema/versions/SBZV_5_rollback.sql @@ -1,2 +1,2 @@ ALTER TABLE diva_profile - DROP skn_eqp, \ No newline at end of file + 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 index 63ff6b3..d417506 100644 --- a/core/data/schema/versions/SBZV_6_upgrade.sql +++ b/core/data/schema/versions/SBZV_6_upgrade.sql @@ -1,2 +1,2 @@ ALTER TABLE diva_profile - ADD skn_eqp INT NOT NULL DEFAULT 0, \ No newline at end of file + ADD skn_eqp INT NOT NULL DEFAULT 0; \ 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