diff --git a/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py b/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py new file mode 100644 index 0000000..77ca08a --- /dev/null +++ b/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py @@ -0,0 +1,52 @@ +"""Mai2 PRiSM support + +Revision ID: 5cf98cfe52ad +Revises: 263884e774cc +Create Date: 2025-04-08 08:00:51.243089 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5cf98cfe52ad' +down_revision = '263884e774cc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_score_kaleidxscope', + 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' + ) + 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') + op.drop_table('mai2_score_kaleidxscope') + # ### end Alembic commands ### diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 7337191..7121478 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -195,15 +195,15 @@ Config file is located in `config/cxb.yaml`. ### Presents Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add: -| Game Version | Item ID | Item Kind | Item Description | Present Description | -|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------| -| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial | -| | | | | number, for project raputa | +| Game Version | Item ID | Item Kind | Item Description | Present Description | +|--------------|---------|----------------------|--------------------------------------------|----------------------------------------------------------------------------| +| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial number, for project raputa | +| PRiSM (23) | 3 | KaleidxScopeKey (15) | 紫の鍵 (Purple Key) | Officially obtained on the webui with a serial number, for KaleidxScope | ### 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..6a53324 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,9 +84,19 @@ class Mai2Constants: "maimai DX FESTiVAL", "maimai DX FESTiVAL PLUS", "maimai DX BUDDiES", - "maimai DX BUDDiES PLUS" + "maimai DX BUDDiES PLUS", + "maimai DX PRiSM" ) - + KALEIDXSCOPE_KEY_CONDITION={ + 1: [11009, 11008, 11100, 11097, 11098, 11099, 11163, 11162, 11161, 11228, 11229, 11231, 11463, 11464, 11465, 11538, 11539, 11541, 11620, 11622, 11623, 11737, 11738, 11164, 11230, 11466, 11540, 11621, 11739], + #青の扉: Played 29 songs + 2: [11102, 11234, 11300, 11529, 11542, 11612], + #白の扉: set Frame as "Latent Kingdom" (459504), play 3 or 4 songs by the composer 大国奏音 in 1 pc + 3: [], + #紫の扉: need to enter redeem code 51090942171709440000 + 4: [11023, 11106, 11221, 11222, 11300, 11374, 11458, 11523, 11619, 11663, 11746], + #青の扉: Played 11 songs + } MAI_VERSION_LUT = { "100": VER_MAIMAI, "110": VER_MAIMAI_PLUS, 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..95ebb74 --- /dev/null +++ b/titles/mai2/prism.py @@ -0,0 +1,107 @@ +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}, + ] + } + + async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: + # kaleidxscope keyget condition judgement + # player may get key before GateFound + for gate in range(1,5): + if gate == 1 or gate == 4: + condition_satisfy = 0 + for condition in Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]: + score_list = await self.data.score.get_best_scores(user_id=data["userId"], song_id=condition) + if score_list: + condition_satisfy = condition_satisfy + 1 + if len(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]) == condition_satisfy: + new_kaleidxscope = {'gateId': gate, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + elif gate == 2: + user_profile = await self.data.profile.get_profile_detail(user_id=data["userId"], version=self.version) + user_frame = user_profile["frameId"] + if user_frame == 459504: + playlogs = await self.data.score.get_playlogs(user_id=data["userId"], idx=0, limit=0) + + playlog_dict = {} + for playlog in playlogs: + playlog_id = playlog["playlogId"] + if playlog_id not in playlog_dict: + playlog_dict[playlog_id] = [] + playlog_dict[playlog_id].append(playlog["musicId"]) + valid_playlogs = [] + allowed_music = set(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[2]) + for playlog_id, music_ids in playlog_dict.items(): + + if len(music_ids) != len(set(music_ids)): + continue + all_valid = True + for mid in music_ids: + if mid not in allowed_music: + all_valid = False + break + if all_valid: + valid_playlogs.append(playlog_id) + + if valid_playlogs: + new_kaleidxscope = {'gateId': 2, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + 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