From 75842b5a886b3f61fdf50c361894a9887af39618 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Tue, 11 Jul 2023 09:12:34 +0000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 9681f86e33738d0c825055d85eb4922fc0c16ee0 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Thu, 5 Oct 2023 07:56:46 +0000 Subject: [PATCH 8/8] 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.