[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: Hay1tsme/artemis#177
Co-authored-by: ppc <albie@ppc.moe>
Co-committed-by: ppc <albie@ppc.moe>
This commit is contained in:
ppc
2024-09-23 17:21:29 +00:00
committed by Hay1tsme
parent e85728f33c
commit f47175a144
13 changed files with 246 additions and 11 deletions

View File

@ -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')

View File

@ -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")

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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