From f47175a1440bc71cad51b2cd56fcda0ff4afb89d Mon Sep 17 00:00:00 2001 From: ppc Date: Mon, 23 Sep 2024 17:21:29 +0000 Subject: [PATCH] [mai2] add buddies plus support (#177) Adds favorite music support (there's an option in the results screen to star a song), handlers for new methods and fixes upsert failures for `userFavoriteList`. The `UserIntimateApi` has been added but didn't seem to add any data during testing, and `CreateTokenApi`/`RemoveTokenApi` have also been added but I think they're only used during guest play. --- Tested on 1.45 with no errors/game crashes (see logs). Card Maker hasn't been tested as I don't have a setup to play with. Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/177 Co-authored-by: ppc Co-committed-by: ppc --- .../28443e2da5b8_mai2_buddies_plus.py | 28 +++++++++ .../versions/54a84103b84e_mai2_intimacy.py | 43 +++++++++++++ ...c91c1206dca_mai2_favorite_song_ordering.py | 24 ++++++++ docs/game_specific_info.md | 1 + readme.md | 1 + titles/cm/read.py | 1 + titles/mai2/base.py | 2 +- titles/mai2/buddiesplus.py | 60 +++++++++++++++++++ titles/mai2/const.py | 4 +- titles/mai2/dx.py | 31 +++++++++- titles/mai2/index.py | 8 ++- titles/mai2/schema/item.py | 12 ++-- titles/mai2/schema/profile.py | 42 ++++++++++++- 13 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 core/data/alembic/versions/28443e2da5b8_mai2_buddies_plus.py create mode 100644 core/data/alembic/versions/54a84103b84e_mai2_intimacy.py create mode 100644 core/data/alembic/versions/bc91c1206dca_mai2_favorite_song_ordering.py create mode 100644 titles/mai2/buddiesplus.py diff --git a/core/data/alembic/versions/28443e2da5b8_mai2_buddies_plus.py b/core/data/alembic/versions/28443e2da5b8_mai2_buddies_plus.py new file mode 100644 index 0000000..42fcdde --- /dev/null +++ b/core/data/alembic/versions/28443e2da5b8_mai2_buddies_plus.py @@ -0,0 +1,28 @@ +"""mai2_buddies_plus + +Revision ID: 28443e2da5b8 +Revises: 5ea73f89d982 +Create Date: 2024-09-15 20:44:02.351819 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '28443e2da5b8' +down_revision = '5ea73f89d982' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('mai2_profile_detail', sa.Column('point', sa.Integer())) + op.add_column('mai2_profile_detail', sa.Column('totalPoint', sa.Integer())) + op.add_column('mai2_profile_detail', sa.Column('friendRegistSkip', sa.SmallInteger())) + + +def downgrade(): + op.drop_column('mai2_profile_detail', 'point') + op.drop_column('mai2_profile_detail', 'totalPoint') + op.drop_column('mai2_profile_detail', 'friendRegistSkip') diff --git a/core/data/alembic/versions/54a84103b84e_mai2_intimacy.py b/core/data/alembic/versions/54a84103b84e_mai2_intimacy.py new file mode 100644 index 0000000..a180bbb --- /dev/null +++ b/core/data/alembic/versions/54a84103b84e_mai2_intimacy.py @@ -0,0 +1,43 @@ +"""mai2_intimacy + +Revision ID: 54a84103b84e +Revises: bc91c1206dca +Create Date: 2024-09-16 17:47:49.164546 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import Column, Integer, UniqueConstraint + +# revision identifiers, used by Alembic. +revision = '54a84103b84e' +down_revision = 'bc91c1206dca' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "mai2_user_intimate", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("partnerId", Integer, nullable=False), + Column("intimateLevel", Integer, nullable=False), + Column("intimateCountRewarded", Integer, nullable=False), + UniqueConstraint("user", "partnerId", name="mai2_user_intimate_uk"), + mysql_charset="utf8mb4", + ) + + op.create_foreign_key( + None, + "mai2_user_intimate", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + +def downgrade(): + op.drop_table("mai2_user_intimate") diff --git a/core/data/alembic/versions/bc91c1206dca_mai2_favorite_song_ordering.py b/core/data/alembic/versions/bc91c1206dca_mai2_favorite_song_ordering.py new file mode 100644 index 0000000..abf6357 --- /dev/null +++ b/core/data/alembic/versions/bc91c1206dca_mai2_favorite_song_ordering.py @@ -0,0 +1,24 @@ +"""mai2_favorite_song_ordering + +Revision ID: bc91c1206dca +Revises: 28443e2da5b8 +Create Date: 2024-09-16 14:24:56.714066 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bc91c1206dca' +down_revision = '28443e2da5b8' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('mai2_item_favorite_music', sa.Column('orderId', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('mai2_item_favorite_music', 'orderId') diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 37561fc..a0aed71 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -218,6 +218,7 @@ Presents are items given to the user when they login, with a little animation (f | SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 20 | maimai DX FESTiVAL PLUS | | SDEZ | 21 | maimai DX BUDDiES | +| SDEZ | 22 | maimai DX BUDDiES PLUS | ### Importer diff --git a/readme.md b/readme.md index c792928..0564c70 100644 --- a/readme.md +++ b/readme.md @@ -50,6 +50,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + FESTiVAL + FESTiVAL PLUS + BUDDiES + + BUDDiES PLUS + O.N.G.E.K.I. + SUMMER diff --git a/titles/cm/read.py b/titles/cm/read.py index 2b5ec8a..b4b3b5e 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -207,6 +207,7 @@ class CardMakerReader(BaseReader): "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, + "1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS } for root, dirs, files in os.walk(base_dir): diff --git a/titles/mai2/base.py b/titles/mai2/base.py index bbd074f..b041028 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -922,7 +922,7 @@ class Mai2Base: 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"]}) + id_list.append({"orderId": fav["orderId"] or 0, "id": fav["musicId"]}) if len(id_list) >= 100: # Lazy but whatever break diff --git a/titles/mai2/buddiesplus.py b/titles/mai2/buddiesplus.py new file mode 100644 index 0000000..e87fae6 --- /dev/null +++ b/titles/mai2/buddiesplus.py @@ -0,0 +1,60 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.buddies import Mai2Buddies +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2BuddiesPlus(Mai2Buddies): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_BUDDIES_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) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.45.00" + return user_data + + async def handle_get_game_weekly_data_api_request(self, data: Dict) -> Dict: + return { + "gameWeeklyData": { + "missionCategory": 0, + "updateDate": "2024-03-21 09:00:00", + "beforeDate": "2099-12-31 00:00:00" + } + } + + async def handle_create_token_api_request(self, data: Dict) -> Dict: + return { + "Bearer": "ARTEMiSTOKEN" # duplicate of handle_user_login_api_request from Mai2Festival + } + + async def handle_remove_token_api_request(self, data: Dict) -> Dict: + return {} + + async def handle_get_user_friend_bonus_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "returnCode": 1, + "getMiles": 0 + } + + async def handle_get_user_shop_stock_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "userShopStockList": [] + } + + async def handle_get_user_mission_data_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "userMissionDataList": [], + "userWeeklyData": { + "lastLoginWeek": "2024-03-21 09:00:00", + "beforeLoginWeek": "2099-12-31 00:00:00", + "friendBonusFlag": False + } + } diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 4dc10ce..0d13a0d 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -55,6 +55,7 @@ class Mai2Constants: VER_MAIMAI_DX_FESTIVAL = 19 VER_MAIMAI_DX_FESTIVAL_PLUS = 20 VER_MAIMAI_DX_BUDDIES = 21 + VER_MAIMAI_DX_BUDDIES_PLUS = 22 VERSION_STRING = ( "maimai", @@ -78,7 +79,8 @@ class Mai2Constants: "maimai DX UNiVERSE PLUS", "maimai DX FESTiVAL", "maimai DX FESTiVAL PLUS", - "maimai DX BUDDiES" + "maimai DX BUDDiES", + "maimai DX BUDDiES PLUS" ) @classmethod diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 7a067d7..66bf914 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -242,7 +242,13 @@ class Mai2DX(Mai2Base): if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: - await self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + kind_id = fav.get("kind", fav.get("itemKind")) # itemKind key used in BUDDiES+ + if kind_id is not None: + await self.data.item.put_favorite(user_id, kind_id, fav["itemIdList"]) + + if "userFavoritemusicList" in upsert and len(upsert["userFavoritemusicList"]) > 0: + for fav in upsert["userFavoritemusicList"]: + await self.data.item.add_fav_music(user_id, fav["id"], fav["orderId"]) if ( "userFriendSeasonRankingList" in upsert @@ -259,6 +265,11 @@ class Mai2DX(Mai2Base): if "user2pPlaylog" in upsert: await self.data.score.put_playlog_2p(user_id, upsert["user2pPlaylog"]) + # added in BUDDiES+ + if "userIntimateList" in upsert and len(upsert["userIntimateList"]) > 0: + for intimate in upsert["userIntimateList"]: + await self.data.profile.put_intimacy(user_id, intimate["partnerId"], intimate["intimateLevel"], intimate["intimateCountRewarded"]) + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} async def handle_get_user_data_api_request(self, data: Dict) -> Dict: @@ -708,6 +719,24 @@ class Mai2DX(Mai2Base): ret['loginId'] = ret.get('loginCount', 0) return ret + # Intimate api added in BUDDiES+ + async def handle_get_user_intimate_api_request(self, data: Dict) -> Dict: + intimate = await self.data.profile.get_intimacy(data["userId"]) + if intimate is None: + return {} + + partner_list = [{ + "partnerId": i["partnerId"], + "intimateLevel": i["intimateLevel"], + "intimateCountRewarded": i["intimateCountRewarded"] + } for i in intimate] + + return { + "userId": data["userId"], + "length": len(partner_list), + "userIntimateList": partner_list + } + # 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) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index ad02648..e8b88ec 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -30,6 +30,7 @@ from .universeplus import Mai2UniversePlus from .festival import Mai2Festival from .festivalplus import Mai2FestivalPlus from .buddies import Mai2Buddies +from .buddiesplus import Mai2BuddiesPlus class Mai2Servlet(BaseServlet): @@ -64,7 +65,8 @@ class Mai2Servlet(BaseServlet): Mai2UniversePlus, Mai2Festival, Mai2FestivalPlus, - Mai2Buddies + Mai2Buddies, + Mai2BuddiesPlus ] self.logger = logging.getLogger("mai2") @@ -302,8 +304,10 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL elif version >= 135 and version < 140: # FESTiVAL PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS - elif version >= 140: # BUDDiES + elif version >= 140 and version < 145: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + elif version >= 145: # BUDDiES PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS elif game_code == "SDGA": # Int if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d53ebbc..87ddca4 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -144,6 +144,7 @@ fav_music = Table( nullable=False, ), Column("musicId", Integer, nullable=False), + Column("orderId", Integer, nullable=True), UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"), mysql_charset="utf8mb4", ) @@ -453,10 +454,10 @@ class Mai2ItemData(BaseData): self, user_id: int, kind: int, item_id_list: List[int] ) -> Optional[int]: sql = insert(favorite).values( - user=user_id, kind=kind, item_id_list=item_id_list + user=user_id, itemKind=kind, itemIdList=item_id_list ) - conflict = sql.on_duplicate_key_update(item_id_list=item_id_list) + conflict = sql.on_duplicate_key_update(itemIdList=item_id_list) result = await self.execute(conflict) if result is None: @@ -484,13 +485,14 @@ class Mai2ItemData(BaseData): if result: return result.fetchall() - async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]: + async def add_fav_music(self, user_id: int, music_id: int, order_id: Optional[int] = None) -> Optional[int]: sql = insert(fav_music).values( user = user_id, - musicId = music_id + musicId = music_id, + orderId = order_id ) - conflict = sql.on_duplicate_key_update(musicId = music_id) + conflict = sql.on_duplicate_key_update(orderId = order_id) result = await self.execute(conflict) if result: diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index c191a1a..3ff85d2 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -3,7 +3,7 @@ from titles.mai2.const import Mai2Constants from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger, SmallInteger from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.engine import Row @@ -43,6 +43,8 @@ detail = Table( Column("currentPlayCount", Integer), # new with buddies Column("renameCredit", Integer), # new with buddies Column("mapStock", Integer), # new with fes+ + Column("point", Integer), # new with buddies+ + Column("totalPoint", Integer), # new with buddies+ Column("eventWatchedDate", String(25)), Column("lastGameId", String(25)), Column("lastRomVersion", String(25)), @@ -97,6 +99,7 @@ detail = Table( Column("playerOldRating", BigInteger), Column("playerNewRating", BigInteger), Column("dateTime", BigInteger), + Column("friendRegistSkip", SmallInteger), # new with buddies+ Column("banState", Integer), # new with uni+ UniqueConstraint("user", "version", name="mai2_profile_detail_uk"), mysql_charset="utf8mb4", @@ -510,6 +513,22 @@ rival = Table( mysql_charset="utf8mb4", ) +intimacy = Table( +"mai2_user_intimate", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("partnerId", Integer, nullable=False), + Column("intimateLevel", Integer, nullable=False), + Column("intimateCountRewarded", Integer, nullable=False), + UniqueConstraint("user", "partnerId", name="mai2_user_intimate_uk"), + mysql_charset="utf8mb4", +) + class Mai2ProfileData(BaseData): async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]: result = await self.execute(detail.select(detail.c.user == user_id)) @@ -905,6 +924,27 @@ class Mai2ProfileData(BaseData): if not result: self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!") + async def get_intimacy(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(intimacy.select(intimacy.c.user == user_id)) + if result: + return result.fetchall() + + async def put_intimacy(self, user_id: int, partner_id: int, level: int, count_rewarded: int) -> Optional[int]: + sql = insert(intimacy).values( + user = user_id, + partnerId = partner_id, + intimateLevel = level, + intimateCountRewarded = count_rewarded + ) + + conflict = sql.on_duplicate_key_update(intimateLevel = level, intimateCountRewarded = count_rewarded) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to update intimacy for user {user_id} and partner {partner_id}!") + async def update_name(self, user_id: int, new_name: str) -> bool: sql = detail.update(detail.c.user == user_id).values( userName=new_name