diff --git a/core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py b/core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py new file mode 100644 index 0000000..b8baa1a --- /dev/null +++ b/core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py @@ -0,0 +1,50 @@ +"""Mai2 Kaleidx Scope Support + +Revision ID: 16f34bf7b968 +Revises: d0f1c7fa9505 +Create Date: 2025-04-02 07:06:15.829591 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '16f34bf7b968' +down_revision = 'd0f1c7fa9505' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_score_kaleidx_scope', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('gateId', sa.Integer(), nullable=True), + sa.Column('isGateFound', sa.Boolean(), nullable=True), + sa.Column('isKeyFound', sa.Boolean(), nullable=True), + sa.Column('isClear', sa.Boolean(), nullable=True), + sa.Column('totalRestLife', sa.Integer(), nullable=True), + sa.Column('totalAchievement', sa.Integer(), nullable=True), + sa.Column('totalDeluxscore', sa.Integer(), nullable=True), + sa.Column('bestAchievement', sa.Integer(), nullable=True), + sa.Column('bestDeluxscore', sa.Integer(), nullable=True), + sa.Column('bestAchievementDate', sa.String(length=25), nullable=True), + sa.Column('bestDeluxscoreDate', sa.String(length=25), nullable=True), + sa.Column('playCount', sa.Integer(), nullable=True), + sa.Column('clearDate', sa.String(length=25), nullable=True), + sa.Column('lastPlayDate', sa.String(length=25), nullable=True), + sa.Column('isInfoWatched', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'gateId', name='mai2_score_best_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_score_kaleidx_scope') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py b/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py new file mode 100644 index 0000000..c879706 --- /dev/null +++ b/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py @@ -0,0 +1,28 @@ +"""Mai2 add PRiSM support + +Revision ID: d0f1c7fa9505 +Revises: 1d0014d35220 +Create Date: 2025-04-02 06:37:10.657372 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd0f1c7fa9505' +down_revision = '1d0014d35220' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('mai2_playlog', sa.Column('extBool2', sa.Boolean(), nullable=True,server_default=sa.text("NULL"))) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('mai2_playlog', 'extBool2') + # ### end Alembic commands ### diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 7337191..6bb3f67 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -203,7 +203,7 @@ Presents are items given to the user when they login, with a little animation (f ### Versions | Game Code | Version ID | Version Name | -| --------- | ---------- | ----------------------- | +|-----------|------------|-------------------------| | SBXL | 0 | maimai | | SBXL | 1 | maimai PLUS | | SBZF | 2 | maimai GreeN | @@ -227,6 +227,8 @@ Presents are items given to the user when they login, with a little animation (f | SDEZ | 20 | maimai DX FESTiVAL PLUS | | SDEZ | 21 | maimai DX BUDDiES | | SDEZ | 22 | maimai DX BUDDiES PLUS | +| SDEZ | 23 | maimai DX PRiSM | + ### Importer diff --git a/readme.md b/readme.md index 57f81a4..e29784d 100644 --- a/readme.md +++ b/readme.md @@ -52,6 +52,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + FESTiVAL PLUS + BUDDiES + BUDDiES PLUS + + PRiSM + O.N.G.E.K.I. + SUMMER diff --git a/titles/cm/read.py b/titles/cm/read.py index d0db43c..8a1bb84 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -207,7 +207,8 @@ 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 + "1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, + "1.50": Mai2Constants.VER_MAIMAI_DX_PRISM } for root, dirs, files in os.walk(base_dir): diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 68d3e80..df35da3 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -59,6 +59,7 @@ class Mai2Constants: VER_MAIMAI_DX_FESTIVAL_PLUS = 20 VER_MAIMAI_DX_BUDDIES = 21 VER_MAIMAI_DX_BUDDIES_PLUS = 22 + VER_MAIMAI_DX_PRISM = 23 VERSION_STRING = ( "maimai", @@ -83,7 +84,8 @@ class Mai2Constants: "maimai DX FESTiVAL", "maimai DX FESTiVAL PLUS", "maimai DX BUDDiES", - "maimai DX BUDDiES PLUS" + "maimai DX BUDDiES PLUS", + "maimai DX PRiSM" ) MAI_VERSION_LUT = { diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index b37a3f4..9b8b547 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -112,6 +112,17 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + # Exp version use this instead of UploadUserPlaylogApi in 1.50 + async def handle_upload_user_playlog_list_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + playlog_list = data["userPlaylogList"] + + for playlog in playlog_list: + await self.data.score.put_playlog(user_id, playlog) + + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -271,6 +282,12 @@ class Mai2DX(Mai2Base): for intimate in upsert["userIntimateList"]: await self.data.profile.put_intimacy(user_id, intimate["partnerId"], intimate["intimateLevel"], intimate["intimateCountRewarded"]) + # added in PRiSM + if "userKaleidxScopeList" in upsert and len(upsert["userKaleidxScopeList"]) > 0: + for kaleidxscope in upsert["userKaleidxScopeList"]: + await self.data.score.put_user_kaleidxscope(user_id, kaleidxscope) + + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} async def handle_get_user_data_api_request(self, data: Dict) -> Dict: diff --git a/titles/mai2/index.py b/titles/mai2/index.py index e8b88ec..86923e7 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -31,6 +31,7 @@ from .festival import Mai2Festival from .festivalplus import Mai2FestivalPlus from .buddies import Mai2Buddies from .buddiesplus import Mai2BuddiesPlus +from .prism import Mai2Prism class Mai2Servlet(BaseServlet): @@ -66,7 +67,8 @@ class Mai2Servlet(BaseServlet): Mai2Festival, Mai2FestivalPlus, Mai2Buddies, - Mai2BuddiesPlus + Mai2BuddiesPlus, + Mai2Prism ] self.logger = logging.getLogger("mai2") @@ -306,8 +308,11 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS 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 version >= 145 and version <150: # BUDDiES PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, + elif version >=150: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif game_code == "SDGA": # Int if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX @@ -325,6 +330,12 @@ 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 and version < 145: # BUDDiES + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + elif version >= 145 and version <150: # BUDDiES PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, + elif version >=150: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM 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/mai2/prism.py b/titles/mai2/prism.py new file mode 100644 index 0000000..5db7c8a --- /dev/null +++ b/titles/mai2/prism.py @@ -0,0 +1,66 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.buddiesplus import Mai2BuddiesPlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Prism(Mai2BuddiesPlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_PRISM + + 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.50.00" + return user_data + + + async def handle_get_user_new_item_list_api_request(self, data: Dict) -> Dict: + return { + "user_id": data["userId"], + "userItemList": [] + } + + #seems to be used for downloading music scores online + async def handle_get_game_music_score_api_request(self, data: Dict) -> Dict: + return { + "gameMusicScore": { + "musicId": data["musicId"], + "level": data["level"], + "type": data["type"], + "scoreData": "" + } + } + + async def handle_get_game_kaleidx_scope_api_request(self, data: Dict) -> Dict: + return { + "gameKaleidxScopeList": [ + {"gateId": 1, "phaseId": 6}, + {"gateId": 2, "phaseId": 6}, + {"gateId": 3, "phaseId": 6}, + {"gateId": 4, "phaseId": 6}, + {"gateId": 5, "phaseId": 6}, + {"gateId": 6, "phaseId": 6} + ] + } + + async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: + kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"]) + + if kaleidxscope is None: + return {"userId": data["userId"], "userKaleidxScopeList":[]} + + kaleidxscope_list = [] + for kaleidxscope_data in kaleidxscope: + tmp = kaleidxscope_data._asdict() + tmp.pop("user") + tmp.pop("id") + kaleidxscope_list.append(tmp) + return { + "userId": data["userId"], + "userKaleidxScopeList": kaleidxscope_list + } \ No newline at end of file diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index cbe7448..d03dba4 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -1,3 +1,4 @@ +from configparser import Interpolation from typing import Dict, List, Optional from sqlalchemy import Column, Table, UniqueConstraint, and_ @@ -147,6 +148,7 @@ playlog = Table( Column("extNum2", Integer), Column("extNum4", Integer), Column("extBool1", Boolean), # new with buddies + Column("extBool2", Boolean), # new with prism Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) @@ -173,6 +175,34 @@ playlog_2p = Table( mysql_charset="utf8mb4", ) +kaleidxscope = Table( + "mai2_score_kaleidxscope", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gateId", Integer), + Column("isGateFound", Boolean), + Column("isKeyFound", Boolean), + Column("isClear", Boolean), + Column("totalRestLife", Integer), + Column("totalAchievement", Integer), + Column("totalDeluxscore", Integer), + Column("bestAchievement", Integer), + Column("bestDeluxscore", Integer), + Column("bestAchievementDate", String(25)), + Column("bestDeluxscoreDate", String(25)), + Column("playCount", Integer), + Column("clearDate", String(25)), + Column("lastPlayDate", String(25)), + Column("isInfoWatched", Boolean), + UniqueConstraint("user", "gateId", name="mai2_score_best_uk"), + mysql_charset="utf8mb4" +) + course = Table( "mai2_score_course", metadata, @@ -450,3 +480,22 @@ class Mai2ScoreData(BaseData): self.logger.warning(f"aime_id {aime_id} has no playlog ") return None return result.scalar() + + async def get_user_kaleidxscope_list(self, user_id: int) -> Optional[List[Row]]: + sql = kaleidxscope.select(kaleidxscope.c.user == user_id) + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def put_user_kaleidxscope(self, user_id: int, user_kaleidxscope_data: Dict) -> Optional[int]: + user_kaleidxscope_data["user"] = user_id + sql = insert(kaleidxscope).values(**user_kaleidxscope_data) + + conflict = sql.on_duplicate_key_update(**user_kaleidxscope_data) + + result = await self.execute(conflict) + if result is None: + self.logger.error(f"put_user_kaleidxscope: Failed to insert! user_id {user_id}") + return None + return result.lastrowid \ No newline at end of file