diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 638d993..16c15e7 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -60,13 +60,14 @@ Games listed below have been tested and confirmed working. ### SDHD/SDBT -| Version ID | Version Name | -| ---------- | ------------------- | -| 11 | CHUNITHM NEW!! | -| 12 | CHUNITHM NEW PLUS!! | -| 13 | CHUNITHM SUN | -| 14 | CHUNITHM SUN PLUS | -| 15 | CHUNITHM LUMINOUS | +| Version ID | Version Name | +| ---------- | ---------------------- | +| 11 | CHUNITHM NEW!! | +| 12 | CHUNITHM NEW PLUS!! | +| 13 | CHUNITHM SUN | +| 14 | CHUNITHM SUN PLUS | +| 15 | CHUNITHM LUMINOUS | +| 16 | CHUNITHM LUMINOUS PLUS | ### Importer diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index ce2f683..a3781d6 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -40,6 +40,9 @@ version: 15: rom: 2.20.00 data: 2.20.00 + 16: + rom: 2.25.00 + data: 2.25.00 crypto: encrypted_only: False diff --git a/readme.md b/readme.md index e863e52..e44c137 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + SUN + SUN PLUS + LUMINOUS + + LUMINOUS PLUS + crossbeats REV. + Crossbeats REV. diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 37bcbb3..5bec5bf 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -8,7 +8,7 @@ import pytz from core.config import CoreConfig from titles.chuni.config import ChuniConfig -from titles.chuni.const import ChuniConstants, ItemKind +from titles.chuni.const import ChuniConstants, FavoriteItemKind, ItemKind from titles.chuni.database import ChuniData @@ -1014,6 +1014,28 @@ class ChuniBase: ) await self.data.profile.put_net_battle(user_id, net_battle) + # New in LUMINOUS PLUS + if "userFavoriteMusicList" in upsert: + # musicId, orderId + music_ids = set(int(m["musicId"]) for m in upsert["userFavoriteMusicList"]) + current_favorites = await self.data.item.get_all_favorites( + user_id, self.version, fav_kind=FavoriteItemKind.MUSIC + ) + + if current_favorites is None: + current_favorites = [] + + current_favorite_ids = set(x.favId for x in current_favorites) + keep_ids = current_favorite_ids.intersection(music_ids) + deleted_ids = current_favorite_ids - keep_ids + added_ids = music_ids - keep_ids + + for fav_id in deleted_ids: + await self.data.item.delete_favorite_music(user_id, self.version, fav_id) + + for fav_id in added_ids: + await self.data.item.put_favorite_music(user_id, self.version, fav_id) + return {"returnCode": "1"} async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 45fd498..d0d73d5 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -25,6 +25,7 @@ class ChuniConstants: VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 VER_CHUNITHM_LUMINOUS = 15 + VER_CHUNITHM_LUMINOUS_PLUS = 16 VERSION_NAMES = [ "CHUNITHM", @@ -43,6 +44,7 @@ class ChuniConstants: "CHUNITHM SUN", "CHUNITHM SUN PLUS", "CHUNITHM LUMINOUS", + "CHUNITHM LUMINOUS PLUS", ] SCORE_RANK_INTERVALS_OLD = [ @@ -98,6 +100,7 @@ class MapAreaConditionType(IntEnum): TROPHY_OBTAINED = 3 + RANK_SSSP = 18 RANK_SSS = 19 RANK_SSP = 20 RANK_SS = 21 @@ -127,7 +130,7 @@ class ItemKind(IntEnum): FRAME = 2 """ "Frame" is the background for the gauge/score/max combo display - shown during gameplay. This item cannot be equipped (as of LUMINOUS) + shown during gameplay. This item cannot be equipped (as of LUMINOUS PLUS) and is hardcoded to the current game's version. """ @@ -146,7 +149,7 @@ class ItemKind(IntEnum): ULTIMA_UNLOCK = 12 """This only applies to ULTIMA difficulties that are *not* unlocked by - SS-ing EXPERT+MASTER. + reaching S rank on EXPERT difficulty or above. """ diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 144f770..080c041 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -36,6 +36,7 @@ from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus from .luminous import ChuniLuminous +from .luminousplus import ChuniLuminousPlus class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -64,6 +65,7 @@ class ChuniServlet(BaseServlet): ChuniSun, ChuniSunPlus, ChuniLuminous, + ChuniLuminousPlus, ] self.logger = logging.getLogger("chuni") @@ -107,6 +109,7 @@ class ChuniServlet(BaseServlet): f"{ChuniConstants.VER_CHUNITHM_SUN_PLUS}_int": 36, ChuniConstants.VER_CHUNITHM_LUMINOUS: 8, f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8, + ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56, } for version, keys in self.game_cfg.crypto.keys.items(): @@ -235,8 +238,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_SUN elif version >= 215 and version < 220: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS - elif version >= 220: # LUMINOUS + elif version >= 220 and version < 225: # LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS + elif version >= 225: # LUMINOUS PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS elif game_code == "SDGS": # Int if version < 105: # SUPERSTAR internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS @@ -250,8 +255,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_SUN elif version >= 125 and version < 130: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS - elif version >= 130: # LUMINOUS + elif version >= 130 and version < 135: # LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS + elif version >= 135: # LUMINOUS PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're @@ -311,8 +318,10 @@ class ChuniServlet(BaseServlet): return Response(zlib.compress(b'{"stat": "0"}')) try: - unzip = zlib.decompress(req_raw) - + if request.headers.get("x-debug") is not None: + unzip = req_raw + else: + unzip = zlib.decompress(req_raw) except zlib.error as e: self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" @@ -352,6 +361,9 @@ class ChuniServlet(BaseServlet): self.logger.debug(f"Response {resp}") + if request.headers.get("x-debug") is not None: + return Response(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped: diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py index 8f02820..6fcc9ea 100644 --- a/titles/chuni/luminous.py +++ b/titles/chuni/luminous.py @@ -2,9 +2,13 @@ from datetime import timedelta from typing import Dict from core.config import CoreConfig -from titles.chuni.sunplus import ChuniSunPlus -from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType from titles.chuni.config import ChuniConfig +from titles.chuni.const import ( + ChuniConstants, + MapAreaConditionLogicalOperator, + MapAreaConditionType, +) +from titles.chuni.sunplus import ChuniSunPlus class ChuniLuminous(ChuniSunPlus): @@ -18,7 +22,7 @@ class ChuniLuminous(ChuniSunPlus): # Does CARD MAKER 1.35 work this far up? user_data["lastDataVersion"] = "2.20.00" return user_data - + async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict: user_id = data["userId"] mission_id = data["missionId"] @@ -28,7 +32,7 @@ class ChuniLuminous(ChuniSunPlus): mission_data = await self.data.item.get_cmission(user_id, mission_id) progress_data = await self.data.item.get_cmission_progress(user_id, mission_id) - + if mission_data and progress_data: point = mission_data["point"] @@ -48,12 +52,14 @@ class ChuniLuminous(ChuniSunPlus): "userCMissionProgressList": progress_list, } - async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict: + async def handle_get_user_net_battle_ranking_info_api_request( + self, data: Dict + ) -> Dict: user_id = data["userId"] net_battle = {} net_battle_data = await self.data.profile.get_net_battle(user_id) - + if net_battle_data: net_battle = { "isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"], @@ -94,131 +100,135 @@ class ChuniLuminous(ChuniSunPlus): # (event ID 14214) was imported into ARTEMiS, we disable the requirement # for this trophy. if 14214 in event_by_id: - mission_in_progress_end_date = (event_by_id[14214]["startDate"] - timedelta(hours=2)).strftime(self.date_time_format) - - conditions.extend([ - { - "mapAreaId": 2206201, # BlythE ULTIMA - "length": 1, - # Obtain the trophy "MISSION in progress". - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6832, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": mission_in_progress_end_date, - } - ], - }, - { - "mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA - "length": 1, - # Obtain the trophy "MISSION in progress". - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6832, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": mission_in_progress_end_date, - } - ], - }, - { - "mapAreaId": 2206203, # New York Back Raise - "length": 1, - # SS NightTheater's EXPERT chart and get the title - # "今宵、劇場に映し出される景色とは――――。" - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6833, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - }, - ], - }, - { - "mapAreaId": 2206204, # Spasmodic - "length": 2, - # - Get 1 miss on Random (any difficulty) and get the title "当たり待ち" - # - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を" - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6834, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - }, - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6835, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - }, - ], - }, - { - "mapAreaId": 2206205, # ΩΩPARTS - "length": 2, - # - S Sage EXPERT to get the title "マターリ進行キボンヌ" - # - Equip this title and play cab-to-cab with another person with this title - # to get "マターリしようよ". Disabled because it is difficult to play cab2cab - # on data setups. A network operator may consider re-enabling it by uncommenting - # the second condition. - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6836, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - }, - # { - # "type": MapAreaConditionType.TROPHY_OBTAINED.value, - # "conditionId": 6837, - # "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - # "startDate": start_date, - # "endDate": "2099-12-31 00:00:00.0", - # }, - ], - }, - { - "mapAreaId": 2206206, # Blow My Mind - "length": 1, - # SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT - # to get the title "Can you hear me?" - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.TROPHY_OBTAINED.value, - "conditionId": 6838, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - }, - ], - }, - { - "mapAreaId": 2206207, # VALLIS-NERIA - "length": 6, - # Finish the 6 other areas - "mapAreaConditionList": [ - { - "type": MapAreaConditionType.MAP_AREA_CLEARED.value, - "conditionId": x, - "logicalOpe": MapAreaConditionLogicalOperator.AND.value, - "startDate": start_date, - "endDate": "2099-12-31 00:00:00.0", - } - for x in range(2206201, 2206207) - ], - }, - ]) - + mission_in_progress_end_date = ( + event_by_id[14214]["startDate"] - timedelta(hours=2) + ).strftime(self.date_time_format) + + conditions.extend( + [ + { + "mapAreaId": 2206201, # BlythE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206203, # New York Back Raise + "length": 1, + # SS NightTheater's EXPERT chart and get the title + # "今宵、劇場に映し出される景色とは――――。" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6833, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206204, # Spasmodic + "length": 2, + # - Get 1 miss on Random (any difficulty) and get the title "当たり待ち" + # - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6834, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6835, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206205, # ΩΩPARTS + "length": 2, + # - S Sage EXPERT to get the title "マターリ進行キボンヌ" + # - Equip this title and play cab-to-cab with another person with this title + # to get "マターリしようよ". Disabled because it is difficult to play cab2cab + # on data setups. A network operator may consider re-enabling it by uncommenting + # the second condition. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6836, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + # { + # "type": MapAreaConditionType.TROPHY_OBTAINED.value, + # "conditionId": 6837, + # "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + # "startDate": start_date, + # "endDate": "2099-12-31 00:00:00.0", + # }, + ], + }, + { + "mapAreaId": 2206206, # Blow My Mind + "length": 1, + # SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT + # to get the title "Can you hear me?" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6838, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206207, # VALLIS-NERIA + "length": 6, + # Finish the 6 other areas + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_AREA_CLEARED.value, + "conditionId": x, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + for x in range(2206201, 2206207) + ], + }, + ] + ) + # LUMINOUS ep. I if 14005 in event_by_id: start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format) @@ -226,7 +236,7 @@ class ChuniLuminous(ChuniSunPlus): if not mystic_area_1_added: conditions.append(mystic_area_1_conditions) mystic_area_1_added = True - + mystic_area_1_conditions["length"] += 1 mystic_area_1_conditions["mapAreaConditionList"].append( { @@ -254,15 +264,15 @@ class ChuniLuminous(ChuniSunPlus): ], } ) - + # LUMINOUS ep. II if 14251 in event_by_id: start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format) - + if not mystic_area_1_added: conditions.append(mystic_area_1_conditions) mystic_area_1_added = True - + mystic_area_1_conditions["length"] += 1 mystic_area_1_conditions["mapAreaConditionList"].append( { @@ -291,6 +301,203 @@ class ChuniLuminous(ChuniSunPlus): } ) + # LUMINOUS ep. III + if 14481 in event_by_id: + start_date = event_by_id[14481]["startDate"].strftime(self.date_time_format) + + if not mystic_area_1_added: + conditions.append(mystic_area_1_conditions) + mystic_area_1_added = True + + mystic_area_1_conditions["length"] += 1 + mystic_area_1_conditions["mapAreaConditionList"].append( + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020703, + "logicalOpe": MapAreaConditionLogicalOperator.OR.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + ) + + conditions.append( + { + "mapAreaId": 3229304, # Mystic Rainbow of LUMINOUS Area 4, + "length": 1, + # Unlocks when LUMINOUS ep. III is completed. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020703, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ) + + # 1UM1N0U5 ep. 111 + if 14483 in event_by_id: + start_date = event_by_id[14483]["startDate"].replace( + hour=0, minute=0, second=0 + ) + + # conditions to unlock the 6 "Key of ..." area in the map + # for the first 14 days: Defandour MASTER AJ, crazy (about you) MASTER AJ, Halcyon ULTIMA SSS + title_conditions = [ + { + "type": MapAreaConditionType.ALL_JUSTICE.value, + "conditionId": 258103, # Defandour MASTER + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": ( + start_date + timedelta(days=14) - timedelta(seconds=1) + ).strftime(self.date_time_format), + }, + { + "type": MapAreaConditionType.ALL_JUSTICE.value, + "conditionId": 258003, # crazy (about you) MASTER + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": ( + start_date + timedelta(days=14) - timedelta(seconds=1) + ).strftime(self.date_time_format), + }, + { + "type": MapAreaConditionType.RANK_SSS.value, + "conditionId": 17304, # Halcyon ULTIMA + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": ( + start_date + timedelta(days=14) - timedelta(seconds=1) + ).strftime(self.date_time_format), + }, + ] + + # For each next 14 days, the conditions are lowered to SS+, S+, S, and then always unlocked + for i, typ in enumerate( + [ + MapAreaConditionType.RANK_SSP.value, + MapAreaConditionType.RANK_SP.value, + MapAreaConditionType.RANK_S.value, + MapAreaConditionType.ALWAYS_UNLOCKED.value, + ] + ): + start = (start_date + timedelta(days=14 * (i + 1))).strftime( + self.date_time_format + ) + + if typ != MapAreaConditionType.ALWAYS_UNLOCKED.value: + end = ( + start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1) + ).strftime(self.date_time_format) + + title_conditions.extend( + [ + { + "type": typ, + "conditionId": condition_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start, + "endDate": end, + } + for condition_id in {17304, 258003, 258103} + ] + ) + else: + end = "2099-12-31 00:00:00" + + title_conditions.append( + { + "type": typ, + "conditionId": 0, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start, + "endDate": end, + } + ) + + # actually add all the conditions + for map_area_id in range(3229201, 3229207): + conditions.append( + { + "mapAreaId": map_area_id, + "length": len(title_conditions), + "mapAreaConditionList": title_conditions, + } + ) + + # Ultimate Force + # For the first 14 days, the condition is to obtain all 9 "Key of ..." titles + # Afterwards, the condition is the 6 "Key of ..." titles that you can obtain + # by playing the 6 areas, as well as obtaining specific ranks on + # [CRYSTAL_ACCESS] / Strange Love / βlαnoir + ultimate_force_conditions = [] + + # Trophies obtained by playing the 6 areas + for trophy_id in {6851, 6853, 6855, 6857, 6858, 6860}: + ultimate_force_conditions.append( + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": trophy_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": "2099-12-31 00:00:00", + } + ) + + # βlαnoir MASTER SSS+ / Strange Love MASTER SSS+ / [CRYSTAL_ACCESS] MASTER SSS+ + for trophy_id in {6852, 6854, 6856}: + ultimate_force_conditions.append( + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": trophy_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": ( + start_date + timedelta(days=14) - timedelta(seconds=1) + ).strftime(self.date_time_format), + } + ) + + # For each next 14 days, the rank conditions for the 3 songs lowers + # Finally, the Ultimate Force area is unlocked as soon as you finish the 6 other areas. + for i, typ in enumerate( + [ + MapAreaConditionType.RANK_SSS.value, + MapAreaConditionType.RANK_SS.value, + MapAreaConditionType.RANK_S.value, + ] + ): + start = (start_date + timedelta(days=14 * (i + 1))).strftime( + self.date_time_format + ) + + end = ( + start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1) + ).strftime(self.date_time_format) + + ultimate_force_conditions.extend( + [ + { + "type": typ, + "conditionId": condition_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start, + "endDate": end, + } + for condition_id in {109403, 212103, 244203} + ] + ) + + conditions.append( + { + "mapAreaId": 3229207, + "length": len(ultimate_force_conditions), + "mapAreaConditionList": ultimate_force_conditions, + } + ) return { "length": len(conditions), diff --git a/titles/chuni/luminousplus.py b/titles/chuni/luminousplus.py new file mode 100644 index 0000000..659b39d --- /dev/null +++ b/titles/chuni/luminousplus.py @@ -0,0 +1,170 @@ +from datetime import timedelta +from typing import Dict + +from core.config import CoreConfig +from titles.chuni.config import ChuniConfig +from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType +from titles.chuni.luminous import ChuniLuminous + + +class ChuniLuminousPlus(ChuniLuminous): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # Does CARD MAKER 1.35 work this far up? + user_data["lastDataVersion"] = "2.25.00" + return user_data + + async def handle_get_user_c_mission_list_api_request(self, data: Dict) -> Dict: + user_id = int(data["userId"]) + user_mission_list_request = data["userCMissionList"] + + user_mission_list = [] + + for request in user_mission_list_request: + user_id = int(request["userId"]) + mission_id = int(request["missionId"]) + point = int(request["point"]) + + mission_data = await self.data.item.get_cmission(user_id, mission_id) + progress_data = await self.data.item.get_cmission_progress(user_id, mission_id) + + if mission_data is None or progress_data is None: + continue + + point = mission_data.point + user_mission_progress_list = [ + { + "order": progress.order, + "stage": progress.stage, + "progress": progress.progress, + } + for progress in progress_data + ] + + user_mission_list.append( + { + "userId": user_id, + "missionId": mission_id, + "point": point, + "userCMissionProgressList": user_mission_progress_list, + }, + ) + + return { + "userId": user_id, + "userCMissionList": user_mission_list, + } + + async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict: + # There is no game data for this, everything is server side. + # However, we can selectively show/hide events as data is imported into the server. + events = await self.data.static.get_enabled_events(self.version) + event_by_id = {evt["eventId"]: evt for evt in events} + conditions = [] + + # LUMINOUS ep. Ascension + if ep_ascension := event_by_id.get(15512): + start_date = ep_ascension["startDate"].replace(hour=0, minute=0, second=0) + + # Finish LUMINOUS ep. VII to unlock LUMINOUS ep. Ascension. + task_track_map_conditions = [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020707, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date.strftime(self.date_time_format), + "endDate": "2099-12-31 00:00:00", + } + ] + + # You also need to reach a specific rank on Acid God MASTER. + # This condition lowers every 7 days. + # After the first 4 weeks, you only need to finish ep. VII. + for i, typ in enumerate([ + MapAreaConditionType.RANK_SSSP.value, + MapAreaConditionType.RANK_SSS.value, + MapAreaConditionType.RANK_SS.value, + MapAreaConditionType.RANK_S.value, + ]): + start = start_date + timedelta(days=7 * i) + end = start_date + timedelta(days=7 * (i + 1)) - timedelta(seconds=1) + + task_track_map_conditions.append( + { + "type": typ, + "conditionId": 265103, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start.strftime(self.date_time_format), + "endDate": end.strftime(self.date_time_format), + } + ) + + conditions.extend( + [ + { + "mapAreaId": map_area_id, + "length": len(task_track_map_conditions), + "mapAreaConditionList": task_track_map_conditions, + } + for map_area_id in {3220801, 3220802, 3220803, 3220804} + ] + ) + + # To unlock the final map area (Forsaken Tale), achieve a specific rank + # on the 4 task tracks in the previous map areas. This condition also lowers + # every 7 days, similar to Acid God. + # After 28 days, you only need to finish the other 4 areas in ep. Ascension. + forsaken_tale_conditions = [] + + for i, typ in enumerate([ + MapAreaConditionType.RANK_SSSP.value, + MapAreaConditionType.RANK_SSS.value, + MapAreaConditionType.RANK_SS.value, + MapAreaConditionType.RANK_S.value, + ]): + start = start_date + timedelta(days=7 * i) + end = start_date + timedelta(days=7 * (i + 1)) - timedelta(seconds=1) + + forsaken_tale_conditions.extend( + [ + { + "type": typ, + "conditionId": condition_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start.strftime(self.date_time_format), + "endDate": end.strftime(self.date_time_format), + } + for condition_id in {98203, 108603, 247503, 233903} + ] + ) + + forsaken_tale_conditions.extend( + [ + { + "type": MapAreaConditionType.MAP_AREA_CLEARED.value, + "conditionId": map_area_id, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": (start_date + timedelta(days=28)).strftime(self.date_time_format), + "endDate": "2099-12-31 00:00:00", + } + for map_area_id in {3220801, 3220802, 3220803, 3220804} + ] + ) + + conditions.append( + { + "mapAreaId": 3220805, + "length": len(forsaken_tale_conditions), + "mapAreaConditionList": forsaken_tale_conditions, + } + ) + + return { + "length": len(conditions), + "gameMapAreaConditionList": conditions, + } diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 15d2b6c..a3aa1a3 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -36,6 +36,8 @@ class ChuniNew(ChuniBase): return "215" if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: return "220" + if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: + return "225" async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9