From e6cab6fd94edda65499fc63f61940951f5edfe41 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Wed, 1 Oct 2025 02:16:53 +0000 Subject: [PATCH] Hook up BEMAPI client support to data layer, use it to allow bootstrapping Dance Evolution from a remote server. --- bemani/data/api/client.py | 4 ++ bemani/data/api/music.py | 80 +++++++++++++++++++++++++++++++++++++++ bemani/data/api/user.py | 7 ++++ bemani/utils/read.py | 22 +++++++++-- 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/bemani/data/api/client.py b/bemani/data/api/client.py index 1fafa69..fd9ebc1 100644 --- a/bemani/data/api/client.py +++ b/bemani/data/api/client.py @@ -147,6 +147,7 @@ class APIClient: GameConstants.POPN_MUSIC: "popnmusic", GameConstants.REFLEC_BEAT: "reflecbeat", GameConstants.SDVX: "soundvoltex", + GameConstants.DANCE_EVOLUTION: "danceevolution", }.get(game) if servergame is None: raise UnsupportedRequestAPIException("The client does not support this game/version!") @@ -217,6 +218,9 @@ class APIClient: VersionConstants.SDVX_GRAVITY_WARS: "3", VersionConstants.SDVX_HEAVENLY_HAVEN: "4", }, + GameConstants.DANCE_EVOLUTION: { + VersionConstants.DANCE_EVOLUTION: "1", + } } .get(game, {}) .get(version) diff --git a/bemani/data/api/music.py b/bemani/data/api/music.py index ba3e4ee..0ac28fd 100644 --- a/bemani/data/api/music.py +++ b/bemani/data/api/music.py @@ -38,6 +38,34 @@ class GlobalMusicData(BaseGlobalData): def __max(self, int1: int, int2: int) -> int: return max(int1, int2) + def __format_danevo_score(self, version: int, songid: int, songchart: int, data: Dict[str, Any]) -> Score: + grade = { + "AAA": DBConstants.DANEVO_GRADE_AAA, + "AA": DBConstants.DANEVO_GRADE_AA, + "A": DBConstants.DANEVO_GRADE_A, + "B": DBConstants.DANEVO_GRADE_B, + "C": DBConstants.DANEVO_GRADE_C, + "D": DBConstants.DANEVO_GRADE_D, + "E": DBConstants.DANEVO_GRADE_E, + "F": DBConstants.DANEVO_GRADE_FAILED, + }.get(data.get("grade"), DBConstants.DANEVO_GRADE_FAILED) + + return Score( + -1, + songid, + songchart, + int(data.get("points", 0)), + int(data.get("timestamp", -1)), + self.__max(int(data.get("timestamp", -1)), int(data.get("updated", -1))), + -1, # No location for remote play + 1, # No play info for remote play + { + "combo": int(data.get("combo", -1)), + "grade": grade, + "full_combo": bool(data.get("full_combo", False)), + }, + ) + def __format_ddr_score(self, version: int, songid: int, songchart: int, data: Dict[str, Any]) -> Score: halo = { "none": DBConstants.DDR_HALO_NONE, @@ -317,8 +345,30 @@ class GlobalMusicData(BaseGlobalData): return self.__format_reflec_score(version, songid, songchart, data) if game == GameConstants.SDVX: return self.__format_sdvx_score(version, songid, songchart, data) + if game == GameConstants.DANCE_EVOLUTION: + return self.__format_danevo_score(version, songid, songchart, data) return None + def __merge_danevo_score(self, version: int, oldscore: Score, newscore: Score) -> Score: + return Score( + -1, + oldscore.id, + oldscore.chart, + self.__max(oldscore.points, newscore.points), + self.__max(oldscore.timestamp, newscore.timestamp), + self.__max( + self.__max(oldscore.update, newscore.update), + self.__max(oldscore.timestamp, newscore.timestamp), + ), + oldscore.location, # Always propagate location from local setup if possible + oldscore.plays + newscore.plays, + { + "grade": self.__max(oldscore.data["grade"], newscore.data["grade"]), + "combo": self.__max(oldscore.data["combo"], newscore.data["combo"]), + "full_combo": oldscore.data["full_combo"] or newscore.data["full_combo"], + }, + ) + def __merge_ddr_score(self, version: int, oldscore: Score, newscore: Score) -> Score: return Score( -1, @@ -513,6 +563,8 @@ class GlobalMusicData(BaseGlobalData): return self.__merge_reflec_score(version, oldscore, newscore) if game == GameConstants.SDVX: return self.__merge_sdvx_score(version, oldscore, newscore) + if game == GameConstants.DANCE_EVOLUTION: + return self.__merge_danevo_score(version, oldscore, newscore) return oldscore @@ -929,6 +981,32 @@ class GlobalMusicData(BaseGlobalData): return retval + def __format_danevo_song( + self, + version: int, + songid: int, + songchart: int, + name: Optional[str], + artist: Optional[str], + genre: Optional[str], + data: Dict[str, Any], + ) -> Song: + return Song( + game=GameConstants.DANCE_EVOLUTION, + version=version, + songid=songid, + songchart=songchart, + name=name, + artist=artist, + genre=genre, + data={ + "bpm_min": int(data["bpm_min"]), + "bpm_max": int(data["bpm_max"]), + "level": int(data["level"]), + "kcal": float(data["kcal"]), + }, + ) + def __format_ddr_song( self, version: int, @@ -1168,6 +1246,8 @@ class GlobalMusicData(BaseGlobalData): return self.__format_reflec_song(version, songid, songchart, name, artist, genre, data) if game == GameConstants.SDVX: return self.__format_sdvx_song(version, songid, songchart, name, artist, genre, data) + if game == GameConstants.DANCE_EVOLUTION: + return self.__format_danevo_song(version, songid, songchart, name, artist, genre, data) return None def get_all_songs( diff --git a/bemani/data/api/user.py b/bemani/data/api/user.py index 8cd3dff..d451699 100644 --- a/bemani/data/api/user.py +++ b/bemani/data/api/user.py @@ -13,6 +13,11 @@ class GlobalUserData(BaseGlobalData): super().__init__(api) self.user = user + def __format_danevo_profile(self, updates: Profile, profile: Profile) -> None: + area = profile.get_str("area", "") + if area: + updates["area"] = area + def __format_ddr_profile(self, updates: Profile, profile: Profile) -> None: area = profile.get_int("area", -1) if area != -1: @@ -86,6 +91,8 @@ class GlobalUserData(BaseGlobalData): self.__format_reflec_profile(new, profile) if profile.game == GameConstants.SDVX: self.__format_sdvx_profile(new, profile) + if profile.game == GameConstants.DANCE_EVOLUTION: + self.__format_danevo_profile(new, profile) return new diff --git a/bemani/utils/read.py b/bemani/utils/read.py index 1fac9ac..cd56df2 100644 --- a/bemani/utils/read.py +++ b/bemani/utils/read.py @@ -6162,10 +6162,24 @@ class ImportDanceEvolution(ImportBase): return retval def lookup(self, server: str, token: str) -> List[Dict[str, Any]]: - # TODO: We never got far enough to support DanEvo in the server, or - # specify it in BEMAPI. So this is a dead function for now, but maybe - # some year in the future I'll be able to support this. - return [] + # Grab music info from remote server + music = self.remote_music(server, token) + songs = music.get_all_songs(self.game, self.version) + lut: Dict[int, Dict[str, Any]] = {} + for song in songs: + if song.id not in lut: + lut[song.id] = { + "id": song.id, + "title": song.name, + "artist": song.artist, + "level": song.data.get_int("level"), + "bpm_min": song.data.get_int("bpm_min"), + "bpm_max": song.data.get_int("bpm_max"), + "kcal": song.data.get_float("kcal"), + } + + # Return the reassembled data + return [val for _, val in lut.items()] def import_music_db(self, songs: List[Dict[str, Any]]) -> None: for song in songs: