forked from Hay1tsme/artemis
Merge pull request 'Added Team and Rival support to Chunithm' (#24) from EmmyHeart/artemis:develop into develop
Reviewed-on: Hay1tsme/artemis#24
This commit is contained in:
commit
1f65cfd2eb
@ -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
|
- 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
|
- 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 (<user1>, <version>, <user2>, 2);
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user2>, <version>, <user1>, 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 (<teamName>);
|
||||||
|
```
|
||||||
|
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 (<user>, <version>, <songId>, 1);
|
||||||
|
```
|
||||||
|
The songId is based on the actual ID within your version of Chunithm.
|
||||||
|
|
||||||
|
|
||||||
## crossbeats REV.
|
## crossbeats REV.
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ server:
|
|||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
|
||||||
team:
|
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:
|
mods:
|
||||||
use_login_bonus: True
|
use_login_bonus: True
|
||||||
|
@ -361,11 +361,98 @@ class ChuniBase:
|
|||||||
"userDuelList": duel_list,
|
"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:
|
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||||
user_fav_item_list = []
|
user_fav_item_list = []
|
||||||
|
|
||||||
# still needs to be implemented on WebUI
|
# 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(
|
fav_list = self.data.item.get_all_favorites(
|
||||||
data["userId"], self.version, fav_kind=int(data["kind"])
|
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:
|
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
|
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": 0}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"teamRank": 1,
|
"teamRank": team_rank,
|
||||||
"teamName": team_name,
|
"teamName": team_name,
|
||||||
"userTeamPoint": {
|
"userTeamPoint": {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"orderId": 1,
|
"orderId": 1,
|
||||||
"teamPoint": 1,
|
"teamPoint": 1,
|
||||||
"aggrDate": data["playDate"],
|
"aggrDate": data["playDate"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@ -709,9 +814,25 @@ class ChuniBase:
|
|||||||
self.data.score.put_playlog(user_id, playlog)
|
self.data.score.put_playlog(user_id, playlog)
|
||||||
|
|
||||||
if "userTeamPoint" in upsert:
|
if "userTeamPoint" in upsert:
|
||||||
# TODO: team stuff
|
team_points = upsert["userTeamPoint"]
|
||||||
pass
|
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:
|
if "userMapAreaList" in upsert:
|
||||||
for map_area in upsert["userMapAreaList"]:
|
for map_area in upsert["userMapAreaList"]:
|
||||||
self.data.item.put_map_area(user_id, map_area)
|
self.data.item.put_map_area(user_id, map_area)
|
||||||
|
@ -30,6 +30,11 @@ class ChuniTeamConfig:
|
|||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "chuni", "team", "name", default=""
|
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:
|
class ChuniModsConfig:
|
||||||
|
@ -637,3 +637,103 @@ class ChuniProfileData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
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
|
||||||
|
}
|
@ -200,3 +200,10 @@ class ChuniScoreData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
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()
|
@ -453,6 +453,15 @@ class ChuniStaticData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchone()
|
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(
|
def put_avatar(
|
||||||
self,
|
self,
|
||||||
version: int,
|
version: int,
|
||||||
|
Loading…
Reference in New Issue
Block a user