Merge pull request 'mai2:prism_support' (#205) from SoulGateKey/artemis:prism_support into develop

Reviewed-on: Hay1tsme/artemis#205
This commit is contained in:
2025-04-08 03:14:36 +00:00
9 changed files with 262 additions and 11 deletions

View File

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

View File

@ -196,14 +196,14 @@ Config file is located in `config/cxb.yaml`.
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: 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 | | 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 | | BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial number, for project raputa |
| | | | | number, for project raputa | | PRiSM (23) | 3 | KaleidxScopeKey (15) | 紫の鍵 (Purple Key) | Officially obtained on the webui with a serial number, for KaleidxScope |
### Versions ### Versions
| Game Code | Version ID | Version Name | | Game Code | Version ID | Version Name |
| --------- | ---------- | ----------------------- | |-----------|------------|-------------------------|
| SBXL | 0 | maimai | | SBXL | 0 | maimai |
| SBXL | 1 | maimai PLUS | | SBXL | 1 | maimai PLUS |
| SBZF | 2 | maimai GreeN | | 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 | 20 | maimai DX FESTiVAL PLUS |
| SDEZ | 21 | maimai DX BUDDiES | | SDEZ | 21 | maimai DX BUDDiES |
| SDEZ | 22 | maimai DX BUDDiES PLUS | | SDEZ | 22 | maimai DX BUDDiES PLUS |
| SDEZ | 23 | maimai DX PRiSM |
### Importer ### Importer

View File

@ -52,6 +52,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ FESTiVAL PLUS + FESTiVAL PLUS
+ BUDDiES + BUDDiES
+ BUDDiES PLUS + BUDDiES PLUS
+ PRiSM
+ O.N.G.E.K.I. + O.N.G.E.K.I.
+ SUMMER + SUMMER

View File

@ -207,7 +207,8 @@ class CardMakerReader(BaseReader):
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
"1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, "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): for root, dirs, files in os.walk(base_dir):

View File

@ -59,6 +59,7 @@ class Mai2Constants:
VER_MAIMAI_DX_FESTIVAL_PLUS = 20 VER_MAIMAI_DX_FESTIVAL_PLUS = 20
VER_MAIMAI_DX_BUDDIES = 21 VER_MAIMAI_DX_BUDDIES = 21
VER_MAIMAI_DX_BUDDIES_PLUS = 22 VER_MAIMAI_DX_BUDDIES_PLUS = 22
VER_MAIMAI_DX_PRISM = 23
VERSION_STRING = ( VERSION_STRING = (
"maimai", "maimai",
@ -83,9 +84,19 @@ class Mai2Constants:
"maimai DX FESTiVAL", "maimai DX FESTiVAL",
"maimai DX FESTiVAL PLUS", "maimai DX FESTiVAL PLUS",
"maimai DX BUDDiES", "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 = { MAI_VERSION_LUT = {
"100": VER_MAIMAI, "100": VER_MAIMAI,
"110": VER_MAIMAI_PLUS, "110": VER_MAIMAI_PLUS,

View File

@ -112,6 +112,17 @@ class Mai2DX(Mai2Base):
return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} 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: async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
user_id = data["userId"] user_id = data["userId"]
charge = data["userCharge"] charge = data["userCharge"]
@ -271,6 +282,12 @@ class Mai2DX(Mai2Base):
for intimate in upsert["userIntimateList"]: for intimate in upsert["userIntimateList"]:
await self.data.profile.put_intimacy(user_id, intimate["partnerId"], intimate["intimateLevel"], intimate["intimateCountRewarded"]) 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"} return {"returnCode": 1, "apiName": "UpsertUserAllApi"}
async def handle_get_user_data_api_request(self, data: Dict) -> Dict: async def handle_get_user_data_api_request(self, data: Dict) -> Dict:

View File

@ -31,6 +31,7 @@ from .festival import Mai2Festival
from .festivalplus import Mai2FestivalPlus from .festivalplus import Mai2FestivalPlus
from .buddies import Mai2Buddies from .buddies import Mai2Buddies
from .buddiesplus import Mai2BuddiesPlus from .buddiesplus import Mai2BuddiesPlus
from .prism import Mai2Prism
class Mai2Servlet(BaseServlet): class Mai2Servlet(BaseServlet):
@ -66,7 +67,8 @@ class Mai2Servlet(BaseServlet):
Mai2Festival, Mai2Festival,
Mai2FestivalPlus, Mai2FestivalPlus,
Mai2Buddies, Mai2Buddies,
Mai2BuddiesPlus Mai2BuddiesPlus,
Mai2Prism
] ]
self.logger = logging.getLogger("mai2") self.logger = logging.getLogger("mai2")
@ -306,8 +308,11 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
elif version >= 140 and version < 145: # BUDDiES elif version >= 140 and version < 145: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >= 145: # BUDDiES PLUS elif version >= 145 and version <150: # BUDDiES PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_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 elif game_code == "SDGA": # Int
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX
@ -325,6 +330,12 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
elif version >= 135 and version < 140: # FESTiVAL PLUS elif version >= 135 and version < 140: # FESTiVAL PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_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 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 # If we get a 32 character long hex string, it's a hash and we're

107
titles/mai2/prism.py Normal file
View File

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

View File

@ -1,3 +1,4 @@
from configparser import Interpolation
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Column, Table, UniqueConstraint, and_ from sqlalchemy import Column, Table, UniqueConstraint, and_
@ -147,6 +148,7 @@ playlog = Table(
Column("extNum2", Integer), Column("extNum2", Integer),
Column("extNum4", Integer), Column("extNum4", Integer),
Column("extBool1", Boolean), # new with buddies Column("extBool1", Boolean), # new with buddies
Column("extBool2", Boolean), # new with prism
Column("trialPlayAchievement", Integer), Column("trialPlayAchievement", Integer),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@ -173,6 +175,34 @@ playlog_2p = Table(
mysql_charset="utf8mb4", 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( course = Table(
"mai2_score_course", "mai2_score_course",
metadata, metadata,
@ -450,3 +480,22 @@ class Mai2ScoreData(BaseData):
self.logger.warning(f"aime_id {aime_id} has no playlog ") self.logger.warning(f"aime_id {aime_id} has no playlog ")
return None return None
return result.scalar() 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