From a2a1a578a46e02dbc43322a5c230d53978161bd0 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Sat, 20 Apr 2024 14:07:37 +0000 Subject: [PATCH] Support CHUNITHM LUMINOUS --- .../c143b80bd966_chunithm_luminous.py | 89 ++++++ example_config/chuni.yaml | 3 + titles/chuni/base.py | 25 ++ titles/chuni/const.py | 4 +- titles/chuni/index.py | 14 +- titles/chuni/luminous.py | 259 ++++++++++++++++++ titles/chuni/schema/item.py | 90 +++++- titles/chuni/schema/profile.py | 33 ++- 8 files changed, 511 insertions(+), 6 deletions(-) create mode 100644 core/data/alembic/versions/c143b80bd966_chunithm_luminous.py create mode 100644 titles/chuni/luminous.py diff --git a/core/data/alembic/versions/c143b80bd966_chunithm_luminous.py b/core/data/alembic/versions/c143b80bd966_chunithm_luminous.py new file mode 100644 index 0000000..c6798da --- /dev/null +++ b/core/data/alembic/versions/c143b80bd966_chunithm_luminous.py @@ -0,0 +1,89 @@ +"""chunithm luminous + +Revision ID: c143b80bd966 +Revises: 6a7e8277763b +Create Date: 2024-04-20 14:06:54.630558 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy import Column, UniqueConstraint +from sqlalchemy.types import Integer, Boolean + + +# revision identifiers, used by Alembic. +revision = 'c143b80bd966' +down_revision = '6a7e8277763b' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "chuni_profile_net_battle", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_profile_net_battle", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission_progress", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission_progress", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + +def downgrade(): + op.drop_table("chuni_profile_net_battle") + op.drop_table("chuni_item_cmission") + op.drop_table("chuni_item_cmission_progress") diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 53da186..4855fa1 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -22,6 +22,9 @@ version: 14: rom: 2.15.00 data: 2.15.00 + 15: + rom: 2.20.00 + data: 2.20.00 crypto: encrypted_only: False diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 83086ff..84989cb 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -938,6 +938,31 @@ class ChuniBase: upsert[rating_type], ) + # added in LUMINOUS + if "userCMissionList" in upsert: + for cmission in upsert["userCMissionList"]: + mission_id = cmission["missionId"] + + await self.data.item.put_cmission( + user_id, + { + "missionId": mission_id, + "point": cmission["point"], + }, + ) + + for progress in cmission["userCMissionProgressList"]: + await self.data.item.put_cmission_progress(user_id, mission_id, progress) + + if "userNetBattleData" in upsert: + net_battle = upsert["userNetBattleData"][0] + + # fix the boolean + net_battle["isRankUpChallengeFailed"] = ( + False if net_battle["isRankUpChallengeFailed"] == "false" else True + ) + await self.data.profile.put_net_battle(user_id, net_battle) + 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 3e83378..ea063fa 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -20,6 +20,7 @@ class ChuniConstants: VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 + VER_CHUNITHM_LUMINOUS = 15 VERSION_NAMES = [ "CHUNITHM", "CHUNITHM PLUS", @@ -35,7 +36,8 @@ class ChuniConstants: "CHUNITHM NEW!!", "CHUNITHM NEW PLUS!!", "CHUNITHM SUN", - "CHUNITHM SUN PLUS" + "CHUNITHM SUN PLUS", + "CHUNITHM_LUMINOUS", ] @classmethod diff --git a/titles/chuni/index.py b/titles/chuni/index.py index ecb6988..72845eb 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -32,6 +32,7 @@ from .new import ChuniNew from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus +from .luminous import ChuniLuminous class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -59,6 +60,7 @@ class ChuniServlet(BaseServlet): ChuniNewPlus, ChuniSun, ChuniSunPlus, + ChuniLuminous, ] self.logger = logger.create_logger( @@ -81,7 +83,9 @@ class ChuniServlet(BaseServlet): for method in method_list: method_fixed = inflection.camelize(method)[6:-7] # number of iterations was changed to 70 in SUN and then to 36 - if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: + if version == ChuniConstants.VER_CHUNITHM_LUMINOUS: + iter_count = 8 + elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: iter_count = 36 elif version == ChuniConstants.VER_CHUNITHM_SUN: iter_count = 70 @@ -173,8 +177,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 210 and version < 215: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 215: # SUN + elif version >= 215 and version < 220: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 220: + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif game_code == "SDGS": # Int if version < 110: # SUPERSTAR internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE" @@ -184,8 +190,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 120 and version < 125: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 125: # SUN PLUS + elif version >= 125 and version < 130: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 130: + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS 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 diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py new file mode 100644 index 0000000..e3f7024 --- /dev/null +++ b/titles/chuni/luminous.py @@ -0,0 +1,259 @@ +from typing import Dict + +from core.config import CoreConfig +from core.utils import Utils +from titles.chuni.sunplus import ChuniSunPlus +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig + + +class ChuniLuminous(ChuniSunPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS + + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[ + "rom" + ] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[ + "data" + ] + + t_port = "" + if ( + not self.core_cfg.server.is_using_proxy + and Utils.get_title_port(self.core_cfg) != 80 + ): + t_port = f":{self.core_cfg.server.port}" + + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/" + return ret + + 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) + + # I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up + user_data["lastDataVersion"] = "2.20.00" + return user_data + + async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict: + return { + "length": 1, + "gameMapAreaConditionList": [ + { + "mapAreaId": "2206201", # BlythE ULTIMA + "length": 1, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6832", # MISSION in progress + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2024-01-25 00:00:00.0", + }, + ], + }, + { + "mapAreaId": "2206202", # PRIVATE SERVICE ULTIMA + "length": 1, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6832", # MISSION in progress + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2024-01-25 00:00:00.0", + }, + ], + }, + { + "mapAreaId": "2206203", # New York Back Raise + "length": 1, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6833", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": "2206204", # Spasmodic + "length": 2, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6834", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6835", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": "2206205", # ΩΩPARTS + "length": 2, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6836", # マターリ進行キボンヌ + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6837", # マターリしようよ + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2024-01-25 00:00:00.0", + }, + ], + }, + { + "mapAreaId": "2206206", # Blow My Mind + "length": 1, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6838", # Can you hear me? + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + # TODO: Proper VALLIS-NERIA: Unlock other areas + # { + # "mapAreaId": "2206207", + # "length": 0, + # "mapAreaConditionList": [ + + # ], + # } + { + "mapAreaId": "2206207", + "length": 7, + "mapAreaConditionList": [ + { + "type": "3", + "conditionId": "6832", # MISSION in progress + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2024-01-25 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6833", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6834", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6835", # 今宵、劇場に映し出される景色とは――――。 + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6836", # マターリ進行キボンヌ + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6837", # マターリしようよ + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2024-01-25 00:00:00.0", + }, + { + "type": "3", + "conditionId": "6838", # Can you hear me? + "logicalOpe": "1", + "startDate": "2023-12-14 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ] + } + + async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + mission_id = data["missionId"] + + progress_list = [] + point = 0 + + 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"] + + for progress in progress_data: + progress_list.append( + { + "order": progress["order"], + "stage": progress["stage"], + "progress": progress["progress"], + } + ) + + return { + "userId": user_id, + "missionId": mission_id, + "point": point, + "userCMissionProgressList": progress_list, + } + + 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"], + "highestBattleRankId": net_battle_data["highestBattleRankId"], + "battleIconId": net_battle_data["battleIconId"], + "battleIconNum": net_battle_data["battleIconNum"], + "avatarEffectPoint": net_battle_data["avatarEffectPoint"], + } + + return { + "userId": user_id, + "userNetBattleData": net_battle, + } diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index f778454..5a2357e 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -242,13 +242,43 @@ matching = Table( mysql_charset="utf8mb4", ) +cmission = Table( + "chuni_item_cmission", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", +) + +cmission_progress = Table( + "chuni_item_cmission_progress", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): async def get_oldest_free_matching(self, version: int) -> Optional[Row]: sql = matching.select( and_( matching.c.version == version, - matching.c.isFull == False + matching.c.isFull == False # noqa: E712 ) ).order_by(matching.c.roomId.asc()) @@ -593,3 +623,61 @@ class ChuniItemData(BaseData): ) return None return result.lastrowid + + async def put_cmission_progress( + self, user_id: int, mission_id: int, progress_data: Dict + ) -> Optional[int]: + progress_data["user"] = user_id + progress_data["missionId"] = mission_id + + sql = insert(cmission_progress).values(**progress_data) + conflict = sql.on_duplicate_key_update(**progress_data) + + result = await self.execute(conflict) + if result is None: + return None + return result.lastrowid + + async def get_cmission_progress( + self, user_id: int, mission_id: int + ) -> Optional[List[Row]]: + sql = cmission_progress.select( + and_( + cmission_progress.c.user == user_id, + cmission_progress.c.missionId == mission_id, + ) + ).order_by(cmission_progress.c.order.asc()) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]: + sql = cmission.select( + and_(cmission.c.user == user_id, cmission.c.missionId == mission_id) + ) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchone() + + async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]: + mission_data["user"] = user_id + + sql = insert(cmission).values(**mission_data) + conflict = sql.on_duplicate_key_update(**mission_data) + + result = await self.execute(conflict) + if result is None: + return None + return result.lastrowid + + async def get_cmissions(self, user_id: int) -> Optional[List[Row]]: + sql = cmission.select(cmission.c.user == user_id) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 341b1b2..52d94fd 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -412,6 +412,19 @@ rating = Table( mysql_charset="utf8mb4", ) +net_battle = Table( + "chuni_profile_net_battle", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", +) + class ChuniProfileData(BaseData): async def update_name(self, user_id: int, new_name: str) -> bool: @@ -729,7 +742,7 @@ class ChuniProfileData(BaseData): total_play_count = 0 for row in playcount_sql: - total_play_count += row[0] + total_play_count = row[0] return { "total_play_count": total_play_count } @@ -757,3 +770,21 @@ class ChuniProfileData(BaseData): return return result.lastrowid + + async def get_net_battle(self, aime_id: int) -> Optional[Row]: + sql = select(net_battle).where(net_battle.c.user == aime_id) + result = await self.execute(sql) + + if result is None: + return None + return result.fetchone() + + async def put_net_battle(self, aime_id: int, net_battle_data: Dict) -> Optional[int]: + net_battle_data["user"] = aime_id + + sql = insert(net_battle).values(**net_battle_data) + conflict = sql.on_duplicate_key_update(**net_battle_data) + + result = await self.execute(conflict) + if result is None: + return None