Merge pull request 'maimai DX FESTiVAL PLUS support' (#44) from Dniel97/artemis:mai2_festival_plus into develop

Reviewed-on: Hay1tsme/artemis#44
This commit is contained in:
Midorica 2023-10-16 20:00:08 +00:00
commit 46f83b9925
15 changed files with 288 additions and 106 deletions

View File

@ -1,6 +1,13 @@
# Changelog # Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20231015
### maimai DX
+ Added support for FESTiVAL PLUS
### Card Maker
+ Added support for maimai DX FESTiVAL PLUS
## 20230716 ## 20230716
### General ### General
+ Docker files added (#19) + Docker files added (#19)

View File

@ -0,0 +1,10 @@
ALTER TABLE mai2_profile_detail
DROP COLUMN mapStock;
ALTER TABLE mai2_profile_extend
DROP COLUMN selectResultScoreViewType;
ALTER TABLE mai2_profile_option
DROP COLUMN outFrameType,
DROP COLUMN touchVolume,
DROP COLUMN breakSlideVolume;

View File

@ -0,0 +1,10 @@
ALTER TABLE mai2_profile_detail
ADD mapStock INT NULL AFTER playCount;
ALTER TABLE mai2_profile_extend
ADD selectResultScoreViewType INT NULL AFTER selectResultDetails;
ALTER TABLE mai2_profile_option
ADD outFrameType INT NULL AFTER dispCenter,
ADD touchVolume INT NULL AFTER slideVolume,
ADD breakSlideVolume INT NULL AFTER slideVolume;

View File

@ -194,18 +194,19 @@ For versions pre-dx
| SDBM | 5 | maimai ORANGE PLUS | | SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK | | SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS | | SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKI | | SDDK | 8 | maimai MURASAKi |
| SDDK | 9 | maimai MURASAKI PLUS | | SDDK | 9 | maimai MURASAKi PLUS |
| SDDZ | 10 | maimai MILK | | SDDZ | 10 | maimai MiLK |
| SDDZ | 11 | maimai MILK PLUS | | SDDZ | 11 | maimai MiLK PLUS |
| SDEY | 12 | maimai FiNALE | | SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX | | SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS | | SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash | | SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS | | SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX Universe | | SDEZ | 17 | maimai DX UNiVERSE |
| SDEZ | 18 | maimai DX Universe PLUS | | SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX Festival | | SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
### Importer ### Importer
@ -347,15 +348,21 @@ python dbutils.py --game SDDT upgrade
### Support status ### Support status
* Card Maker 1.30: #### Card Maker 1.30:
* CHUNITHM NEW!!: Yes * CHUNITHM NEW!!: Yes
* maimai DX UNiVERSE: Yes * maimai DX UNiVERSE: Yes
* O.N.G.E.K.I. bright: Yes * O.N.G.E.K.I. bright: Yes
* Card Maker 1.35: #### Card Maker 1.35:
* CHUNITHM SUN: Yes (NEW PLUS!! up to A032) * CHUNITHM:
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031) * NEW!!: Yes
* O.N.G.E.K.I. bright MEMORY: Yes * NEW PLUS!!: Yes (added in A028)
* SUN: Yes (added in A032)
* maimai DX:
* UNiVERSE PLUS: Yes
* FESTiVAL: Yes (added in A031)
* FESTiVAL PLUS: Yes (added in A035)
* O.N.G.E.K.I. bright MEMORY: Yes
### Importer ### Importer

View File

@ -11,7 +11,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ All versions + omnimix + All versions + omnimix
+ maimai DX + maimai DX
+ All versions up to FESTiVAL + All versions up to FESTiVAL PLUS
+ Hatsune Miku: Project DIVA Arcade + Hatsune Miku: Project DIVA Arcade
+ All versions + All versions

View File

@ -205,6 +205,7 @@ class CardMakerReader(BaseReader):
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
} }
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):

View File

@ -16,4 +16,4 @@ game_codes = [
Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE_GREEN,
Mai2Constants.GAME_CODE, Mai2Constants.GAME_CODE,
] ]
current_schema_version = 7 current_schema_version = 8

View File

@ -20,13 +20,13 @@ class Mai2Constants:
DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
GAME_CODE = "SBXL" GAME_CODE = "SBXL"
GAME_CODE_GREEN = "SBZF" GAME_CODE_GREEN = "SBZF"
GAME_CODE_ORANGE = "SDBM" GAME_CODE_ORANGE = "SDBM"
GAME_CODE_PINK = "SDCQ" GAME_CODE_PINK = "SDCQ"
GAME_CODE_MURASAKI = "SDDK" GAME_CODE_MURASAKI = "SDDK"
GAME_CODE_MILK = "SDDZ" GAME_CODE_MILK = "SDDZ"
GAME_CODE_FINALE = "SDEY" GAME_CODE_FINALE = "SDEY"
GAME_CODE_DX = "SDEZ" GAME_CODE_DX = "SDEZ"
CONFIG_NAME = "mai2.yaml" CONFIG_NAME = "mai2.yaml"
@ -52,6 +52,7 @@ class Mai2Constants:
VER_MAIMAI_DX_UNIVERSE = 17 VER_MAIMAI_DX_UNIVERSE = 17
VER_MAIMAI_DX_UNIVERSE_PLUS = 18 VER_MAIMAI_DX_UNIVERSE_PLUS = 18
VER_MAIMAI_DX_FESTIVAL = 19 VER_MAIMAI_DX_FESTIVAL = 19
VER_MAIMAI_DX_FESTIVAL_PLUS = 20
VERSION_STRING = ( VERSION_STRING = (
"maimai", "maimai",
@ -66,7 +67,7 @@ class Mai2Constants:
"maimai MURASAKi PLUS", "maimai MURASAKi PLUS",
"maimai MiLK", "maimai MiLK",
"maimai MiLK PLUS", "maimai MiLK PLUS",
"maimai FiNALE", "maimai FiNALE",
"maimai DX", "maimai DX",
"maimai DX PLUS", "maimai DX PLUS",
"maimai DX Splash", "maimai DX Splash",
@ -74,6 +75,7 @@ class Mai2Constants:
"maimai DX UNiVERSE", "maimai DX UNiVERSE",
"maimai DX UNiVERSE PLUS", "maimai DX UNiVERSE PLUS",
"maimai DX FESTiVAL", "maimai DX FESTiVAL",
"maimai DX FESTiVAL PLUS",
) )
@classmethod @classmethod

View File

@ -542,8 +542,43 @@ class Mai2DX(Mai2Base):
} }
def handle_get_user_region_api_request(self, data: Dict) -> Dict: def handle_get_user_region_api_request(self, data: Dict) -> Dict:
"""
class UserRegionList:
regionId: int
playCount: int
created: str
"""
return {"userId": data["userId"], "length": 0, "userRegionList": []} return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
rival_id = data["rivalId"]
"""
class UserRivalData:
rivalId: int
rivalName: str
"""
return {"userId": user_id, "userRivalData": {}}
def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
rival_id = data["rivalId"]
next_idx = data["nextIndex"]
rival_music_levels = data["userRivalMusicLevelList"]
"""
class UserRivalMusicList:
class UserRivalMusicDetailList:
level: int
achievement: int
deluxscoreMax: int
musicId: int
userRivalMusicDetailList: list[UserRivalMusicDetailList]
"""
return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict: def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0) user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0) next_index = data.get("nextIndex", 0)

View File

@ -14,7 +14,7 @@ class Mai2Festival(Mai2UniversePlus):
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data) user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35 # hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.30.00" user_data["lastDataVersion"] = "1.30.00"
return user_data return user_data
@ -25,7 +25,13 @@ class Mai2Festival(Mai2UniversePlus):
return user_login return user_login
def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendRateMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendRateMusicIdList": []} return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendSelectionMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}

View File

@ -0,0 +1,38 @@
from typing import Dict
from core.config import CoreConfig
from titles.mai2.festival import Mai2Festival
from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config
class Mai2FestivalPlus(Mai2Festival):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.35.00"
return user_data
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
kind = data.get("kind", 2)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 100)
is_all = data.get("isAllFavoriteItem", False)
"""
class userFavoriteItemList:
orderId: int
id: int
"""
return {
"userId": user_id,
"kind": kind,
"nextIndex": 0,
"userFavoriteItemList": [],
}

View File

@ -23,6 +23,7 @@ from titles.mai2.splashplus import Mai2SplashPlus
from titles.mai2.universe import Mai2Universe from titles.mai2.universe import Mai2Universe
from titles.mai2.universeplus import Mai2UniversePlus from titles.mai2.universeplus import Mai2UniversePlus
from titles.mai2.festival import Mai2Festival from titles.mai2.festival import Mai2Festival
from titles.mai2.festivalplus import Mai2FestivalPlus
class Mai2Servlet: class Mai2Servlet:
@ -47,7 +48,7 @@ class Mai2Servlet:
None, None,
None, None,
None, None,
Mai2Finale, Mai2Finale,
Mai2DX, Mai2DX,
Mai2DXPlus, Mai2DXPlus,
Mai2Splash, Mai2Splash,
@ -55,6 +56,7 @@ class Mai2Servlet:
Mai2Universe, Mai2Universe,
Mai2UniversePlus, Mai2UniversePlus,
Mai2Festival, Mai2Festival,
Mai2FestivalPlus,
] ]
self.logger = logging.getLogger("mai2") self.logger = logging.getLogger("mai2")
@ -110,22 +112,34 @@ class Mai2Servlet:
) )
def setup(self): def setup(self):
if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir): if (
self.game_cfg.uploads.photos
and self.game_cfg.uploads.photos_dir
and not path.exists(self.game_cfg.uploads.photos_dir)
):
try: try:
mkdir(self.game_cfg.uploads.photos_dir) mkdir(self.game_cfg.uploads.photos_dir)
except Exception: except Exception:
self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}") self.logger.error(
f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}"
)
if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir): if (
self.game_cfg.uploads.movies
and self.game_cfg.uploads.movies_dir
and not path.exists(self.game_cfg.uploads.movies_dir)
):
try: try:
mkdir(self.game_cfg.uploads.movies_dir) mkdir(self.game_cfg.uploads.movies_dir)
except Exception: except Exception:
self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}") self.logger.error(
f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}"
)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes: def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.lower() == "ping": if url_path.lower() == "ping":
return zlib.compress(b'{"returnCode": "1"}') return zlib.compress(b'{"returnCode": "1"}')
elif url_path.startswith("api/movie/"): elif url_path.startswith("api/movie/"):
self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}") self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}")
return b"" return b""
@ -140,18 +154,20 @@ class Mai2Servlet:
if request.uri.startswith(b"/SDEZ"): if request.uri.startswith(b"/SDEZ"):
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX
elif version >= 105 and version < 110: # Plus elif version >= 105 and version < 110: # PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
elif version >= 110 and version < 115: # Splash elif version >= 110 and version < 115: # Splash
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
elif version >= 115 and version < 120: # Splash Plus elif version >= 115 and version < 120: # Splash PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
elif version >= 120 and version < 125: # Universe elif version >= 120 and version < 125: # UNiVERSE
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
elif version >= 125 and version < 130: # Universe Plus elif version >= 125 and version < 130: # UNiVERSE PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
elif version >= 130: # Festival elif version >= 130 and version < 135: # FESTiVAL
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
elif version >= 135: # FESTiVAL PLUS
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
else: else:
if version < 110: # 1.0 if version < 110: # 1.0
@ -180,15 +196,20 @@ class Mai2Servlet:
internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS
elif version >= 197: # Finale elif version >= 197: # Finale
internal_ver = Mai2Constants.VER_MAIMAI_FINALE internal_ver = Mai2Constants.VER_MAIMAI_FINALE
if request.getHeader('Mai-Encoding') is not None or request.getHeader('X-Mai-Encoding') is not None: if (
request.getHeader("Mai-Encoding") is not None
or request.getHeader("X-Mai-Encoding") is not None
):
# The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it. # The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it.
# See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove # See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove
# these two(?) headers to not cause issues, but given the general quality of SEGA data... # these two(?) headers to not cause issues, but given the general quality of SEGA data...
enc_ver = request.getHeader('Mai-Encoding') enc_ver = request.getHeader("Mai-Encoding")
if enc_ver is None: if enc_ver is None:
enc_ver = request.getHeader('X-Mai-Encoding') enc_ver = request.getHeader("X-Mai-Encoding")
self.logger.debug(f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}") self.logger.debug(
f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}"
)
try: try:
unzip = zlib.decompress(req_raw) unzip = zlib.decompress(req_raw)
@ -231,10 +252,12 @@ class Mai2Servlet:
self.logger.debug(f"v{version} GET {url_path}") self.logger.debug(f"v{version} GET {url_path}")
url_split = url_path.split("/") url_split = url_path.split("/")
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie": if (url_split[0] == "api" and url_split[1] == "movie") or url_split[
0
] == "movie":
if url_split[2] == "moviestart": if url_split[2] == "moviestart":
return json.dumps({"moviestart":{"status":"OK"}}).encode() return json.dumps({"moviestart": {"status": "OK"}}).encode()
else: else:
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""
@ -251,20 +274,24 @@ class Mai2Servlet:
elif url_split[1].startswith("friend"): elif url_split[1].startswith("friend"):
self.logger.info(f"v{version} old server friend inquire") self.logger.info(f"v{version} old server friend inquire")
return zlib.compress(b"{}") return zlib.compress(b"{}")
else: else:
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""
elif url_split[0] == "usbdl": elif url_split[0] == "usbdl":
if url_split[1] == "CONNECTIONTEST": if url_split[1] == "CONNECTIONTEST":
self.logger.info(f"v{version} usbdl server test") self.logger.info(f"v{version} usbdl server test")
return b"" return b""
elif self.game_cfg.deliver.udbdl_enable and path.exists(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}"): elif self.game_cfg.deliver.udbdl_enable and path.exists(
with open(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", 'rb') as f: f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}"
):
with open(
f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", "rb"
) as f:
return f.read() return f.read()
else: else:
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""
@ -272,12 +299,18 @@ class Mai2Servlet:
elif url_split[0] == "deliver": elif url_split[0] == "deliver":
file = url_split[len(url_split) - 1] file = url_split[len(url_split) - 1]
self.logger.info(f"v{version} {file} deliver inquire") self.logger.info(f"v{version} {file} deliver inquire")
self.logger.debug(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}") self.logger.debug(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"
if self.game_cfg.deliver.enable and path.exists(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"): )
with open(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", 'rb') as f:
if self.game_cfg.deliver.enable and path.exists(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"
):
with open(
f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", "rb"
) as f:
return f.read() return f.read()
else: else:
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""

View File

@ -333,7 +333,7 @@ class Mai2ItemData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchone() return result.fetchone()
def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]:
char_data["user"] = user_id char_data["user"] = user_id
sql = insert(character).values(**char_data) sql = insert(character).values(**char_data)
@ -489,6 +489,8 @@ class Mai2ItemData(BaseData):
else: else:
sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind)) sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind))
sql = sql.order_by(card.c.startDate.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return None return None

View File

@ -40,6 +40,7 @@ detail = Table(
Column("charaLockSlot", JSON), Column("charaLockSlot", JSON),
Column("contentBit", BigInteger), Column("contentBit", BigInteger),
Column("playCount", Integer), Column("playCount", Integer),
Column("mapStock", Integer), # new with fes+
Column("eventWatchedDate", String(25)), Column("eventWatchedDate", String(25)),
Column("lastGameId", String(25)), Column("lastGameId", String(25)),
Column("lastRomVersion", String(25)), Column("lastRomVersion", String(25)),
@ -100,7 +101,7 @@ detail = Table(
) )
detail_old = Table( detail_old = Table(
"maimai_profile_detail", "maimai_profile_detail",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column( Column(
@ -216,6 +217,7 @@ extend = Table(
Column("isPhotoAgree", Boolean), Column("isPhotoAgree", Boolean),
Column("isGotoCodeRead", Boolean), Column("isGotoCodeRead", Boolean),
Column("selectResultDetails", Boolean), Column("selectResultDetails", Boolean),
Column("selectResultScoreViewType", Integer), # new with fes+
Column("sortCategorySetting", Integer), Column("sortCategorySetting", Integer),
Column("sortMusicSetting", Integer), Column("sortMusicSetting", Integer),
Column("selectedCardList", JSON), Column("selectedCardList", JSON),
@ -251,6 +253,7 @@ option = Table(
Column("touchSize", Integer), Column("touchSize", Integer),
Column("starRotate", Integer), Column("starRotate", Integer),
Column("dispCenter", Integer), Column("dispCenter", Integer),
Column("outFrameType", Integer), # new with fes+
Column("dispChain", Integer), Column("dispChain", Integer),
Column("dispRate", Integer), Column("dispRate", Integer),
Column("dispBar", Integer), Column("dispBar", Integer),
@ -276,6 +279,8 @@ option = Table(
Column("exVolume", Integer), Column("exVolume", Integer),
Column("slideSe", Integer), Column("slideSe", Integer),
Column("slideVolume", Integer), Column("slideVolume", Integer),
Column("breakSlideVolume", Integer), # new with fes+
Column("touchVolume", Integer), # new with fes+
Column("touchHoldVolume", Integer), Column("touchHoldVolume", Integer),
Column("damageSeVolume", Integer), Column("damageSeVolume", Integer),
Column("headPhoneVolume", Integer), Column("headPhoneVolume", Integer),
@ -438,7 +443,11 @@ boss = Table(
"maimai_profile_boss", "maimai_profile_boss",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("pandoraFlagList0", Integer), Column("pandoraFlagList0", Integer),
Column("pandoraFlagList1", Integer), Column("pandoraFlagList1", Integer),
Column("pandoraFlagList2", Integer), Column("pandoraFlagList2", Integer),
@ -455,23 +464,32 @@ recent_rating = Table(
"maimai_profile_recent_rating", "maimai_profile_recent_rating",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userRecentRatingList", JSON), Column("userRecentRatingList", JSON),
UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), UniqueConstraint("user", name="mai2_profile_recent_rating_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
consec_logins = Table( consec_logins = Table(
"mai2_profile_consec_logins", "mai2_profile_consec_logins",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column("logins", Integer), Column("logins", Integer),
UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"), UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
class Mai2ProfileData(BaseData): class Mai2ProfileData(BaseData):
def put_profile_detail( def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
@ -494,18 +512,22 @@ class Mai2ProfileData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
def get_profile_detail(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: def get_profile_detail(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx: if is_dx:
sql = ( sql = (
select(detail) select(detail)
.where(and_(detail.c.user == user_id, detail.c.version <= version)) .where(and_(detail.c.user == user_id, detail.c.version <= version))
.order_by(detail.c.version.desc()) .order_by(detail.c.version.desc())
) )
else: else:
sql = ( sql = (
select(detail_old) select(detail_old)
.where(and_(detail_old.c.user == user_id, detail_old.c.version <= version)) .where(
and_(detail_old.c.user == user_id, detail_old.c.version <= version)
)
.order_by(detail_old.c.version.desc()) .order_by(detail_old.c.version.desc())
) )
@ -582,11 +604,15 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warning(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}") self.logger.warning(
f"put_profile_option: failed to update! {user_id} is_dx {is_dx}"
)
return None return None
return result.lastrowid return result.lastrowid
def get_profile_option(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: def get_profile_option(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx: if is_dx:
sql = ( sql = (
select(option) select(option)
@ -596,7 +622,9 @@ class Mai2ProfileData(BaseData):
else: else:
sql = ( sql = (
select(option_old) select(option_old)
.where(and_(option_old.c.user == user_id, option_old.c.version <= version)) .where(
and_(option_old.c.user == user_id, option_old.c.version <= version)
)
.order_by(option_old.c.version.desc()) .order_by(option_old.c.version.desc())
) )
@ -689,7 +717,9 @@ class Mai2ProfileData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def put_web_option(self, user_id: int, version: int, web_opts: Dict) -> Optional[int]: def put_web_option(
self, user_id: int, version: int, web_opts: Dict
) -> Optional[int]:
web_opts["user"] = user_id web_opts["user"] = user_id
web_opts["version"] = version web_opts["version"] = version
sql = insert(web_opt).values(**web_opts) sql = insert(web_opt).values(**web_opts)
@ -698,14 +728,14 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warning( self.logger.warning(f"put_web_option: failed to update! user_id: {user_id}")
f"put_web_option: failed to update! user_id: {user_id}"
)
return None return None
return result.lastrowid return result.lastrowid
def get_web_option(self, user_id: int, version: int) -> Optional[Row]: def get_web_option(self, user_id: int, version: int) -> Optional[Row]:
sql = web_opt.select(and_(web_opt.c.user == user_id, web_opt.c.version == version)) sql = web_opt.select(
and_(web_opt.c.user == user_id, web_opt.c.version == version)
)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@ -725,7 +755,7 @@ class Mai2ProfileData(BaseData):
) )
return None return None
return result.lastrowid return result.lastrowid
def get_grade_status(self, user_id: int) -> Optional[Row]: def get_grade_status(self, user_id: int) -> Optional[Row]:
sql = grade_status.select(grade_status.c.user == user_id) sql = grade_status.select(grade_status.c.user == user_id)
@ -742,12 +772,10 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warning( self.logger.warning(f"put_boss_list: failed to update! user_id: {user_id}")
f"put_boss_list: failed to update! user_id: {user_id}"
)
return None return None
return result.lastrowid return result.lastrowid
def get_boss_list(self, user_id: int) -> Optional[Row]: def get_boss_list(self, user_id: int) -> Optional[Row]:
sql = boss.select(boss.c.user == user_id) sql = boss.select(boss.c.user == user_id)
@ -759,7 +787,7 @@ class Mai2ProfileData(BaseData):
def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]:
sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr)
conflict = sql.on_duplicate_key_update({'userRecentRatingList': rr}) conflict = sql.on_duplicate_key_update({"userRecentRatingList": rr})
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
@ -768,7 +796,7 @@ class Mai2ProfileData(BaseData):
) )
return None return None
return result.lastrowid return result.lastrowid
def get_recent_rating(self, user_id: int) -> Optional[Row]: def get_recent_rating(self, user_id: int) -> Optional[Row]:
sql = recent_rating.select(recent_rating.c.user == user_id) sql = recent_rating.select(recent_rating.c.user == user_id)
@ -776,27 +804,25 @@ class Mai2ProfileData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchone() return result.fetchone()
def add_consec_login(self, user_id: int, version: int) -> None:
sql = insert(consec_logins).values(
user=user_id,
version=version,
logins=1
)
conflict = sql.on_duplicate_key_update( def add_consec_login(self, user_id: int, version: int) -> None:
logins=consec_logins.c.logins + 1 sql = insert(consec_logins).values(user=user_id, version=version, logins=1)
)
conflict = sql.on_duplicate_key_update(logins=consec_logins.c.logins + 1)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.error(f"Failed to update consecutive login count for user {user_id} version {version}") self.logger.error(
f"Failed to update consecutive login count for user {user_id} version {version}"
)
def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: def get_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = select(consec_logins).where(and_( sql = select(consec_logins).where(
consec_logins.c.user==user_id, and_(
consec_logins.c.version==version, consec_logins.c.user == user_id,
)) consec_logins.c.version == version,
)
)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@ -804,12 +830,12 @@ class Mai2ProfileData(BaseData):
return result.fetchone() return result.fetchone()
def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = consec_logins.update(and_( sql = consec_logins.update(
consec_logins.c.user==user_id, and_(
consec_logins.c.version==version, consec_logins.c.user == user_id,
)).values( consec_logins.c.version == version,
logins=1 )
) ).values(logins=1)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:

View File

@ -23,10 +23,11 @@ class Mai2Universe(Mai2SplashPlus):
return { return {
"userName": p["userName"], "userName": p["userName"],
"rating": p["playerRating"], "rating": p["playerRating"],
# hardcode lastDataVersion for CardMaker 1.34 # hardcode lastDataVersion for CardMaker
"lastDataVersion": "1.20.00", "lastDataVersion": "1.20.00",
# checks if the user is still logged in
"isLogin": False, "isLogin": False,
"isExistSellingCard": False, "isExistSellingCard": True,
} }
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
@ -70,8 +71,12 @@ class Mai2Universe(Mai2SplashPlus):
tmp.pop("cardName") tmp.pop("cardName")
tmp.pop("enabled") tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT) tmp["startDate"] = datetime.strftime(
tmp["endDate"] = datetime.strftime(tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT) tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeStartDate"] = datetime.strftime( tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
) )