From 6c0cec4904f5fc9da147e788dcb8ef1a1f8f239e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 19:04:27 -0400 Subject: [PATCH] mai2: add tables for rivals and favorite music --- .../4a02e623e5e6_mai2_add_favs_rivals.py | 48 ++++ .../81e44dd6047a_mai2_buddies_support.py | 2 +- titles/mai2/base.py | 259 ++++++++++++++++++ titles/mai2/schema/item.py | 38 +++ titles/mai2/schema/profile.py | 64 ++++- 5 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py diff --git a/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py new file mode 100644 index 0000000..d221bb6 --- /dev/null +++ b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py @@ -0,0 +1,48 @@ +"""mai2_add_favs_rivals + +Revision ID: 4a02e623e5e6 +Revises: 8ad40a6e7be2 +Create Date: 2024-06-08 19:02:43.856395 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '4a02e623e5e6' +down_revision = '8ad40a6e7be2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_favorite_music', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('musicId', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('mai2_user_rival', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('rival', sa.Integer(), nullable=False), + sa.Column('show', sa.Boolean(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_user_rival') + op.drop_table('mai2_item_favorite_music') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py index 1e24430..7d660b4 100644 --- a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py +++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py @@ -1,7 +1,7 @@ """mai2_buddies_support Revision ID: 81e44dd6047a -Revises: d8950c7ce2fc +Revises: 6a7e8277763b Create Date: 2024-03-12 19:10:37.063907 """ diff --git a/titles/mai2/base.py b/titles/mai2/base.py index f0d6e40..be2794d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -885,3 +885,262 @@ class Mai2Base: self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} + + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 100) # always 100 + is_all = data.get("isAllFavoriteItem", False) # always false + id_list: List[Dict] = [] + + if user_id: + if kind == 1: + fav_music = await self.data.item.get_fav_music(user_id) + if fav_music: + for fav in fav_music: + id_list.append({"orderId": 0, "id": fav["musicId"]}) + if len(id_list) >= 100: # Lazy but whatever + break + + elif kind == 2: + rivals = await self.data.profile.get_rivals_game(user_id) + if rivals: + for rival in rivals: + id_list.append({"orderId": 0, "id": rival["rival"]}) + + return { + "userId": user_id, + "kind": kind, + "nextIndex": 0, + "userFavoriteItemList": id_list, + } + + async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendRateMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendSelectionMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? + user_id = data["userId"] + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } + + # CardMaker support added in Universe + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker + "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version + # checks if the user is still logged in + "isLogin": False, + "isExistSellingCard": True, + } + + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = await self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + async def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = await self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + await self.handle_get_user_item_api_request(data) + + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + + user_card = upsert["userCard"] + await self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, + ) + + # get the profile extend to save the new bought card + extend = await self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: List = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + await self.data.profile.put_profile_extend(user_id, self.version, extend) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), + } + + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index f4048ee..2a1de44 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -134,6 +134,20 @@ favorite = Table( mysql_charset="utf8mb4", ) +fav_music = Table( + "mai2_item_favorite_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer, nullable=False), + UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"), + mysql_charset="utf8mb4", +) + charge = Table( "mai2_item_charge", metadata, @@ -451,6 +465,30 @@ class Mai2ItemData(BaseData): return None return result.fetchall() + async def get_fav_music(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(fav_music.select(fav_music.c.user == user_id)) + if result: + return result.fetchall() + + async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]: + sql = insert(fav_music).values( + user = user_id, + musicId = music_id + ) + + conflict = sql.on_duplicate_key_do_nothing() + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") + + async def remove_fav_music(self, user_id: int, music_id: int) -> None: + result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id))) + if not result: + self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") + async def put_card( self, user_id: int, diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index dbd9845..f82c980 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -187,14 +187,14 @@ ghost = Table( Column("shopId", Integer), Column("regionCode", Integer), Column("typeId", Integer), - Column("musicId", Integer), + Column("rival", Integer), Column("difficulty", Integer), Column("version", Integer), Column("resultBitList", JSON), Column("resultNum", Integer), Column("achievement", Integer), UniqueConstraint( - "user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk" + "user", "version", "rival", "difficulty", name="mai2_profile_ghost_uk" ), mysql_charset="utf8mb4", ) @@ -209,7 +209,7 @@ extend = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectMusicId", Integer), + Column("selectrival", Integer), Column("selectDifficultyId", Integer), Column("categoryIndex", Integer), Column("musicIndex", Integer), @@ -239,7 +239,7 @@ option = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectMusicId", Integer), + Column("selectrival", Integer), Column("optionKind", Integer), Column("noteSpeed", Integer), Column("slideSpeed", Integer), @@ -491,6 +491,24 @@ consec_logins = Table( mysql_charset="utf8mb4", ) +rival = Table( + "mai2_user_rival", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "rival", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("show", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "rival", name="mai2_user_rival_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): async def put_profile_detail( @@ -843,3 +861,41 @@ class Mai2ProfileData(BaseData): if result is None: return None return result.fetchone() + + async def get_rivals(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(rival.c.user == user_id)) + if result: + return result.fetchall() + + async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3)) + if result: + return result.fetchall() + + async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None: + sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values( + show = is_shown + ) + + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}") + + async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values( + user = user_id, + rival = rival_id + ) + + conflict = sql.on_duplicate_key_do_nothing() + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!") + + async def remove_rival(self, user_id: int, rival_id: int) -> None: + result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id))) + if not result: + self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!")