diff --git a/changelog.md b/changelog.md
index 0e8b39a..3f8c6ba 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,7 +1,12 @@
# Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
+## 20240530
+### DIVA
++ Fix reader for when dificulty is not a int
+
## 20240526
+### DIVA
+ Fixed missing awaits causing coroutine error
## 20240524
diff --git a/core/allnet.py b/core/allnet.py
index 861a603..9eb6595 100644
--- a/core/allnet.py
+++ b/core/allnet.py
@@ -960,7 +960,7 @@ class DLReport:
return True
-cfg_dir = environ.get("DIANA_CFG_DIR", "config")
+cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
diff --git a/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py
new file mode 100644
index 0000000..d221bb6
--- /dev/null
+++ b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py
@@ -0,0 +1,48 @@
+"""mai2_add_favs_rivals
+
+Revision ID: 4a02e623e5e6
+Revises: 8ad40a6e7be2
+Create Date: 2024-06-08 19:02:43.856395
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision = '4a02e623e5e6'
+down_revision = '8ad40a6e7be2'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('mai2_item_favorite_music',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user', sa.Integer(), nullable=False),
+ sa.Column('musicId', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'),
+ mysql_charset='utf8mb4'
+ )
+ op.create_table('mai2_user_rival',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user', sa.Integer(), nullable=False),
+ sa.Column('rival', sa.Integer(), nullable=False),
+ sa.Column('show', sa.Boolean(), server_default='0', nullable=False),
+ sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
+ sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'),
+ mysql_charset='utf8mb4'
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('mai2_user_rival')
+ op.drop_table('mai2_item_favorite_music')
+ # ### end Alembic commands ###
diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py
index 04d2217..0a2267c 100644
--- a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py
+++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py
@@ -1,7 +1,7 @@
"""mai2_buddies_support
Revision ID: 81e44dd6047a
-Revises: d8950c7ce2fc
+Revises: 6a7e8277763b
Create Date: 2024-03-12 19:10:37.063907
"""
diff --git a/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py
new file mode 100644
index 0000000..8b6ec51
--- /dev/null
+++ b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py
@@ -0,0 +1,30 @@
+"""ongeki: fix clearStatus
+
+Revision ID: 8ad40a6e7be2
+Revises: 7dc13e364e53
+Create Date: 2024-05-29 19:03:30.062157
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision = '8ad40a6e7be2'
+down_revision = '7dc13e364e53'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.alter_column('ongeki_score_best', 'clearStatus',
+ existing_type=mysql.TINYINT(display_width=1),
+ type_=sa.Integer(),
+ existing_nullable=False)
+
+
+def downgrade():
+ op.alter_column('ongeki_score_best', 'clearStatus',
+ existing_type=sa.Integer(),
+ type_=mysql.TINYINT(display_width=1),
+ existing_nullable=False)
diff --git a/core/data/schema/card.py b/core/data/schema/card.py
index c31e1b2..cd5c647 100644
--- a/core/data/schema/card.py
+++ b/core/data/schema/card.py
@@ -121,7 +121,7 @@ class CardData(BaseData):
result = await self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update last login time for {access_code}")
-
+
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
diff --git a/core/data/schema/user.py b/core/data/schema/user.py
index 5f1ccf2..8c3695c 100644
--- a/core/data/schema/user.py
+++ b/core/data/schema/user.py
@@ -120,3 +120,7 @@ class UserData(BaseData):
result = await self.execute(sql)
return result is not None
+
+ async def get_user_by_username(self, username: str) -> Optional[Row]:
+ result = await self.execute(aime_user.select(aime_user.c.username == username))
+ if result: return result.fetchone()
diff --git a/core/frontend.py b/core/frontend.py
index bb3e9aa..f15f58a 100644
--- a/core/frontend.py
+++ b/core/frontend.py
@@ -44,12 +44,13 @@ class ShopOwner():
self.permissions = perms
class UserSession():
- def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1):
+ def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1):
self.user_id = usr_id
self.current_ip = ip
self.permissions = perms
self.ongeki_version = ongeki_ver
self.chunithm_version = chunithm_ver
+ self.maimai_version = maimai_version
class FrontendServlet():
def __init__(self, cfg: CoreConfig, config_dir: str) -> None:
@@ -192,7 +193,7 @@ class FE_Base():
), media_type="text/html; charset=utf-8")
if sesh is None:
- resp.delete_cookie("DIANA_SESH")
+ resp.delete_cookie("ARTEMIS_SESH")
return resp
def get_routes(self) -> List[Route]:
@@ -216,6 +217,8 @@ class FE_Base():
sesh.current_ip = tk['current_ip']
sesh.permissions = tk['permissions']
sesh.chunithm_version = tk['chunithm_version']
+ sesh.maimai_version = tk['maimai_version']
+ sesh.ongeki_version = tk['ongeki_version']
if sesh.user_id <= 0:
self.logger.error("User session failed to validate due to an invalid ID!")
@@ -241,7 +244,7 @@ class FE_Base():
return UserSession()
def validate_session(self, request: Request) -> Optional[UserSession]:
- sesh = request.cookies.get('DIANA_SESH', "")
+ sesh = request.cookies.get('ARTEMIS_SESH', "")
if not sesh:
return None
@@ -260,7 +263,17 @@ class FE_Base():
def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str:
try:
- return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "chunithm_version": sesh.chunithm_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256")
+ return jwt.encode({
+ "user_id": sesh.user_id,
+ "current_ip": sesh.current_ip,
+ "permissions": sesh.permissions,
+ "ongeki_version": sesh.ongeki_version,
+ "chunithm_version": sesh.chunithm_version,
+ "maimai_version": sesh.maimai_version,
+ "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds },
+ b64decode(self.core_config.frontend.secret),
+ algorithm="HS256"
+ )
except jwt.InvalidKeyError:
self.logger.error("Failed to encode User session because the secret is invalid!")
return ""
@@ -292,7 +305,7 @@ class FE_Gate(FE_Base):
error=err,
sesh=vars(UserSession()),
), media_type="text/html; charset=utf-8")
- resp.delete_cookie("DIANA_SESH")
+ resp.delete_cookie("ARTEMIS_SESH")
return resp
async def render_login(self, request: Request):
@@ -308,8 +321,12 @@ class FE_Gate(FE_Base):
uid = await self.data.card.get_user_id_from_card(access_code)
if uid is None:
- self.logger.debug(f"Failed to find user for card {access_code}")
- return RedirectResponse("/gate/?e=1", 303)
+ user = await self.data.user.get_user_by_username(access_code) # Lookup as username
+ if not user:
+ self.logger.debug(f"Failed to find user for card/username {access_code}")
+ return RedirectResponse("/gate/?e=1", 303)
+
+ uid = user['id']
user = await self.data.user.get_user(uid)
if user is None:
@@ -338,7 +355,7 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303)
- resp.set_cookie("DIANA_SESH", usr_sesh)
+ resp.set_cookie("ARTEMIS_SESH", usr_sesh)
return resp
@@ -377,7 +394,7 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303)
- resp.set_cookie("DIANA_SESH", usr_sesh)
+ resp.set_cookie("ARTEMIS_SESH", usr_sesh)
return resp
@@ -495,7 +512,7 @@ class FE_User(FE_Base):
async def render_logout(self, request: Request):
resp = RedirectResponse("/gate/", 303)
- resp.delete_cookie("DIANA_SESH")
+ resp.delete_cookie("ARTEMIS_SESH")
return resp
async def edit_card(self, request: Request) -> RedirectResponse:
@@ -879,7 +896,7 @@ class FE_Machine(FE_Base):
arcade={}
), media_type="text/html; charset=utf-8")
-cfg_dir = environ.get("DIANA_CFG_DIR", "config")
+cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
diff --git a/core/templates/gate/gate.jinja b/core/templates/gate/gate.jinja
index ca3e2eb..d398cbd 100644
--- a/core/templates/gate/gate.jinja
+++ b/core/templates/gate/gate.jinja
@@ -15,18 +15,18 @@
-moz-appearance: textfield;
}
-
-
*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.
-
*If you have not registered a card with this server, you cannot create a webui account.
{% endblock content %}
\ No newline at end of file
diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py
index b0fa9bc..0dbefac 100644
--- a/titles/chuni/frontend.py
+++ b/titles/chuni/frontend.py
@@ -68,7 +68,7 @@ class ChuniFrontend(FE_Base):
if usr_sesh.chunithm_version >= 0:
encoded_sesh = self.encode_session(usr_sesh)
- resp.set_cookie("DIANA_SESH", encoded_sesh)
+ resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
@@ -240,7 +240,7 @@ class ChuniFrontend(FE_Base):
encoded_sesh = self.encode_session(usr_sesh)
self.logger.info(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/chuni/", 303)
- resp.set_cookie("DIANA_SESH", encoded_sesh)
+ resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)
\ No newline at end of file
diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja
index 69248e8..1854a89 100644
--- a/titles/chuni/templates/chuni_index.jinja
+++ b/titles/chuni/templates/chuni_index.jinja
@@ -5,7 +5,7 @@
{% include 'titles/chuni/templates/chuni_header.jinja' %}
- {% if profile is defined and profile is not none and profile.id > 0 %}
+ {% if profile is defined and profile is not none and profile|length > 0 %}
diff --git a/titles/diva/read.py b/titles/diva/read.py
index 97c9481..64603ca 100644
--- a/titles/diva/read.py
+++ b/titles/diva/read.py
@@ -183,7 +183,11 @@ class DivaReader(BaseReader):
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
for pv_id, pv_data in pv_list.items():
- song_id = int(pv_id.split("_")[1])
+ try:
+ song_id = int(pv_id.split("_")[1])
+ except ValueError:
+ self.logger.error(f"Invalid song ID format: {pv_id}")
+ continue
if "songinfo" not in pv_data:
continue
if "illustrator" not in pv_data["songinfo"]:
diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py
index 74cfddf..234e864 100644
--- a/titles/mai2/__init__.py
+++ b/titles/mai2/__init__.py
@@ -2,10 +2,12 @@ from titles.mai2.index import Mai2Servlet
from titles.mai2.const import Mai2Constants
from titles.mai2.database import Mai2Data
from titles.mai2.read import Mai2Reader
+from .frontend import Mai2Frontend
index = Mai2Servlet
database = Mai2Data
reader = Mai2Reader
+frontend = Mai2Frontend
game_codes = [
Mai2Constants.GAME_CODE_DX,
Mai2Constants.GAME_CODE_FINALE,
diff --git a/titles/mai2/base.py b/titles/mai2/base.py
index 90597d1..4d31213 100644
--- a/titles/mai2/base.py
+++ b/titles/mai2/base.py
@@ -4,6 +4,7 @@ import logging
from base64 import b64decode
from os import path, stat, remove
from PIL import ImageFile
+from random import randint
import pytz
from core.config import CoreConfig
@@ -886,3 +887,45 @@ class Mai2Base:
self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually")
return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'}
+
+ async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
+ user_id = data.get("userId", 0)
+ kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs
+ next_index = data.get("nextIndex", 0)
+ max_ct = data.get("maxCount", 100) # always 100
+ is_all = data.get("isAllFavoriteItem", False) # always false
+ id_list: List[Dict] = []
+
+ if user_id:
+ if kind == 1:
+ 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"]})
+ if len(id_list) >= 100: # Lazy but whatever
+ break
+
+ elif kind == 2:
+ rivals = await self.data.profile.get_rivals_game(user_id)
+ if rivals:
+ for rival in rivals:
+ id_list.append({"orderId": 0, "id": rival["rival"]})
+
+ return {
+ "userId": user_id,
+ "kind": kind,
+ "nextIndex": 0,
+ "userFavoriteItemList": id_list,
+ }
+
+ async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
+ """
+ userRecommendRateMusicIdList: list[int]
+ """
+ return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
+
+ async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
+ """
+ userRecommendSelectionMusicIdList: list[int]
+ """
+ return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}
diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py
index f04b215..38049a1 100644
--- a/titles/mai2/buddies.py
+++ b/titles/mai2/buddies.py
@@ -17,16 +17,3 @@ class Mai2Buddies(Mai2FestivalPlus):
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.40.00"
return user_data
-
- async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
- # TODO: Added in 1.41, implement this?
- user_id = data["userId"]
- version = data.get("version", 1041000)
- user_playlog_list = data.get("userPlaylogList", [])
-
- return {
- "userId": user_id,
- "itemKind": -1,
- "itemId": -1,
- }
-
diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py
index 4423824..04c9172 100644
--- a/titles/mai2/dx.py
+++ b/titles/mai2/dx.py
@@ -563,33 +563,76 @@ class Mai2DX(Mai2Base):
return {"userId": data["userId"], "length": 0, "userRegionList": []}
async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
- user_id = data["userId"]
- rival_id = data["rivalId"]
+ user_id = data.get("userId", 0)
+ rival_id = data.get("rivalId", 0)
- """
- class UserRivalData:
- rivalId: int
- rivalName: str
- """
- return {"userId": user_id, "userRivalData": {}}
+ if not user_id or not rival_id: return {}
+
+ rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version)
+ if not rival_pf: return {}
+
+ return {
+ "userId": user_id,
+ "userRivalData": {
+ "rivalId": rival_id,
+ "rivalName": rival_pf['userName']
+ }
+ }
async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
+ user_id = data.get("userId", 0)
+ rival_id = data.get("rivalId", 0)
+ next_index = data.get("nextIndex", 0)
+ max_ct = 100
+ upper_lim = next_index + max_ct
+ rival_music_list: Dict[int, List] = {}
+
+ songs = await self.data.score.get_best_scores(rival_id)
+ if songs is None:
+ self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!")
+ return {
+ "userId": user_id,
+ "rivalId": rival_id,
+ "nextIndex": 0,
+ "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax
+ }
+
+ num_user_songs = len(songs)
+
+ for x in range(next_index, upper_lim):
+ if x >= num_user_songs:
+ break
+
+ tmp = songs[x]._asdict()
+ if tmp['musicId'] in rival_music_list:
+ rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}])
+
+ else:
+ if len(rival_music_list) >= max_ct:
+ break
+ rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]
+
+ next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim
+ self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
+
+ return {
+ "userId": user_id,
+ "rivalId": rival_id,
+ "nextIndex": next_index,
+ "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()]
+ }
+
+ async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
+ # TODO: Added in 1.41, implement this?
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": []}
+ version = data.get("version", 1041000)
+ user_playlog_list = data.get("userPlaylogList", [])
+
+ return {
+ "userId": user_id,
+ "itemKind": -1,
+ "itemId": -1,
+ }
async def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
@@ -636,3 +679,208 @@ class Mai2DX(Mai2Base):
return ret
ret['loginId'] = ret.get('loginCount', 0)
return ret
+
+ # 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)
+ if p is None:
+ return {}
+
+ return {
+ "userName": p["userName"],
+ "rating": p["playerRating"],
+ # hardcode lastDataVersion for CardMaker
+ "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version
+ # checks if the user is still logged in
+ "isLogin": False,
+ "isExistSellingCard": True,
+ }
+
+ async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
+ # user already exists, because the preview checks that already
+ p = await self.data.profile.get_profile_detail(data["userId"], self.version)
+
+ cards = await self.data.card.get_user_cards(data["userId"])
+ if cards is None or len(cards) == 0:
+ # This should never happen
+ self.logger.error(
+ f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
+ )
+ return {}
+
+ # get the dict representation of the row so we can modify values
+ user_data = p._asdict()
+
+ # remove the values the game doesn't want
+ user_data.pop("id")
+ user_data.pop("user")
+ user_data.pop("version")
+
+ return {"userId": data["userId"], "userData": user_data}
+
+ async def handle_cm_login_api_request(self, data: Dict) -> Dict:
+ return {"returnCode": 1}
+
+ async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
+ return {"returnCode": 1}
+
+ async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
+ selling_cards = await self.data.static.get_enabled_cards(self.version)
+ if selling_cards is None:
+ return {"length": 0, "sellingCardList": []}
+
+ selling_card_list = []
+ for card in selling_cards:
+ tmp = card._asdict()
+ tmp.pop("id")
+ tmp.pop("version")
+ tmp.pop("cardName")
+ tmp.pop("enabled")
+
+ tmp["startDate"] = datetime.strftime(
+ tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+ tmp["endDate"] = datetime.strftime(
+ tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+ tmp["noticeStartDate"] = datetime.strftime(
+ tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+ tmp["noticeEndDate"] = datetime.strftime(
+ tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+
+ selling_card_list.append(tmp)
+
+ return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
+
+ async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
+ user_cards = await self.data.item.get_cards(data["userId"])
+ if user_cards is None:
+ return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
+
+ max_ct = data["maxCount"]
+ next_idx = data["nextIndex"]
+ start_idx = next_idx
+ end_idx = max_ct + start_idx
+
+ if len(user_cards[start_idx:]) > max_ct:
+ next_idx += max_ct
+ else:
+ next_idx = 0
+
+ card_list = []
+ for card in user_cards:
+ tmp = card._asdict()
+ tmp.pop("id")
+ tmp.pop("user")
+
+ tmp["startDate"] = datetime.strftime(
+ tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+ tmp["endDate"] = datetime.strftime(
+ tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
+ )
+ card_list.append(tmp)
+
+ return {
+ "returnCode": 1,
+ "length": len(card_list[start_idx:end_idx]),
+ "nextIndex": next_idx,
+ "userCardList": card_list[start_idx:end_idx],
+ }
+
+ async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
+ await self.handle_get_user_item_api_request(data)
+
+ async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
+ characters = await self.data.item.get_characters(data["userId"])
+
+ chara_list = []
+ for chara in characters:
+ chara_list.append(
+ {
+ "characterId": chara["characterId"],
+ # no clue why those values are even needed
+ "point": 0,
+ "count": 0,
+ "level": chara["level"],
+ "nextAwake": 0,
+ "nextAwakePercent": 0,
+ "favorite": False,
+ "awakening": chara["awakening"],
+ "useCount": chara["useCount"],
+ }
+ )
+
+ return {
+ "returnCode": 1,
+ "length": len(chara_list),
+ "userCharacterList": chara_list,
+ }
+
+ async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
+ return {"length": 0, "userPrintDetailList": []}
+
+ async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
+ user_id = data["userId"]
+ upsert = data["userPrintDetail"]
+
+ # set a random card serial number
+ serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
+
+ # calculate start and end date of the card
+ start_date = datetime.utcnow()
+ end_date = datetime.utcnow() + timedelta(days=15)
+
+ user_card = upsert["userCard"]
+ await self.data.item.put_card(
+ user_id,
+ user_card["cardId"],
+ user_card["cardTypeId"],
+ user_card["charaId"],
+ user_card["mapId"],
+ # add the correct start date and also the end date in 15 days
+ start_date,
+ end_date,
+ )
+
+ # get the profile extend to save the new bought card
+ extend = await self.data.profile.get_profile_extend(user_id, self.version)
+ if extend:
+ extend = extend._asdict()
+ # parse the selectedCardList
+ # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
+ selected_cards: List = extend["selectedCardList"]
+
+ # if no pass is already added, add the corresponding pass
+ if not user_card["cardTypeId"] in selected_cards:
+ selected_cards.insert(0, user_card["cardTypeId"])
+
+ extend["selectedCardList"] = selected_cards
+ await self.data.profile.put_profile_extend(user_id, self.version, extend)
+
+ # properly format userPrintDetail for the database
+ upsert.pop("userCard")
+ upsert.pop("serialId")
+ upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
+
+ await self.data.item.put_user_print_detail(user_id, serial_id, upsert)
+
+ return {
+ "returnCode": 1,
+ "orderId": 0,
+ "serialId": serial_id,
+ "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
+ "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
+ }
+
+ async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
+ return {
+ "returnCode": 1,
+ "orderId": 0,
+ "serialId": data["userPrintlog"]["serialId"],
+ }
+
+ async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
+ return {"returnCode": 1}
diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py
index 94ce3ec..451f0ba 100644
--- a/titles/mai2/festival.py
+++ b/titles/mai2/festival.py
@@ -20,18 +20,6 @@ class Mai2Festival(Mai2UniversePlus):
async def handle_user_login_api_request(self, data: Dict) -> Dict:
user_login = await super().handle_user_login_api_request(data)
- # useless?
+ # TODO: Make use of this
user_login["Bearer"] = "ARTEMiSTOKEN"
return user_login
-
- async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
- """
- userRecommendRateMusicIdList: list[int]
- """
- return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
-
- async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
- """
- userRecommendSelectionMusicIdList: list[int]
- """
- return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}
diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py
index 375d546..3314e34 100644
--- a/titles/mai2/festivalplus.py
+++ b/titles/mai2/festivalplus.py
@@ -17,22 +17,3 @@ class Mai2FestivalPlus(Mai2Festival):
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.35.00"
return user_data
-
- async 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": [],
- }
diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py
new file mode 100644
index 0000000..635c2fa
--- /dev/null
+++ b/titles/mai2/frontend.py
@@ -0,0 +1,190 @@
+from typing import List
+from starlette.routing import Route, Mount
+from starlette.requests import Request
+from starlette.responses import Response, RedirectResponse
+from os import path
+import yaml
+import jinja2
+
+from core.frontend import FE_Base, UserSession
+from core.config import CoreConfig
+from .database import Mai2Data
+from .config import Mai2Config
+from .const import Mai2Constants
+
+class Mai2Frontend(FE_Base):
+ def __init__(
+ self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
+ ) -> None:
+ super().__init__(cfg, environment)
+ self.data = Mai2Data(cfg)
+ self.game_cfg = Mai2Config()
+ if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
+ self.game_cfg.update(
+ yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
+ )
+ self.nav_name = "maimai"
+
+ def get_routes(self) -> List[Route]:
+ return [
+ Route("/", self.render_GET, methods=['GET']),
+ Mount("/playlog", routes=[
+ Route("/", self.render_GET_playlog, methods=['GET']),
+ Route("/{index}", self.render_GET_playlog, methods=['GET']),
+ ]),
+ Route("/update.name", self.update_name, methods=['POST']),
+ Route("/version.change", self.version_change, methods=['POST']),
+ ]
+
+ async def render_GET(self, request: Request) -> bytes:
+ template = self.environment.get_template(
+ "titles/mai2/templates/mai2_index.jinja"
+ )
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ usr_sesh = UserSession()
+
+ if usr_sesh.user_id > 0:
+ versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
+ profile = []
+ if versions:
+ # maimai_version is -1 means it is not initialized yet, select a default version from existing.
+ if usr_sesh.maimai_version < 0:
+ usr_sesh.maimai_version = versions[0]['version']
+ profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version)
+ versions = [x['version'] for x in versions]
+
+ resp = Response(template.render(
+ title=f"{self.core_config.server.name} | {self.nav_name}",
+ game_list=self.environment.globals["game_list"],
+ sesh=vars(usr_sesh),
+ user_id=usr_sesh.user_id,
+ profile=profile,
+ version_list=Mai2Constants.VERSION_STRING,
+ versions=versions,
+ cur_version=usr_sesh.maimai_version
+ ), media_type="text/html; charset=utf-8")
+
+ if usr_sesh.maimai_version >= 0:
+ encoded_sesh = self.encode_session(usr_sesh)
+ resp.delete_cookie("ARTEMIS_SESH")
+ resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
+ return resp
+
+ else:
+ return RedirectResponse("/gate/", 303)
+
+ async def render_GET_playlog(self, request: Request) -> bytes:
+ template = self.environment.get_template(
+ "titles/mai2/templates/mai2_playlog.jinja"
+ )
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ print("wtf")
+ usr_sesh = UserSession()
+
+ if usr_sesh.user_id > 0:
+ if usr_sesh.maimai_version < 0:
+ print(usr_sesh.maimai_version)
+ return RedirectResponse("/game/mai2/", 303)
+ path_index = request.path_params.get('index')
+ if not path_index or int(path_index) < 1:
+ index = 0
+ else:
+ index = int(path_index) - 1 # 0 and 1 are 1st page
+ user_id = usr_sesh.user_id
+ playlog_count = await self.data.score.get_user_playlogs_count(user_id)
+ if playlog_count < index * 20 :
+ return Response(template.render(
+ title=f"{self.core_config.server.name} | {self.nav_name}",
+ game_list=self.environment.globals["game_list"],
+ sesh=vars(usr_sesh),
+ playlog_count=0
+ ), media_type="text/html; charset=utf-8")
+ playlog = await self.data.score.get_playlogs(user_id, index, 20)
+ playlog_with_title = []
+ for record in playlog:
+ music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level)
+ if music_chart:
+ difficultyNum=music_chart.chartId
+ difficulty=music_chart.difficulty
+ artist=music_chart.artist
+ title=music_chart.title
+ else:
+ difficultyNum=0
+ difficulty=0
+ artist="unknown"
+ title="musicid: " + str(record.musicId)
+ playlog_with_title.append({
+ "raw": record,
+ "title": title,
+ "difficultyNum": difficultyNum,
+ "difficulty": difficulty,
+ "artist": artist,
+ })
+ return Response(template.render(
+ title=f"{self.core_config.server.name} | {self.nav_name}",
+ game_list=self.environment.globals["game_list"],
+ sesh=vars(usr_sesh),
+ user_id=usr_sesh.user_id,
+ playlog=playlog_with_title,
+ playlog_count=playlog_count
+ ), media_type="text/html; charset=utf-8")
+ else:
+ return RedirectResponse("/gate/", 303)
+
+ async def update_name(self, request: Request) -> bytes:
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ return RedirectResponse("/gate/", 303)
+
+ form_data = await request.form()
+ new_name: str = form_data.get("new_name")
+ new_name_full = ""
+
+ if not new_name:
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if len(new_name) > 8:
+ return RedirectResponse("/gate/?e=8", 303)
+
+ for x in new_name: # FIXME: This will let some invalid characters through atm
+ o = ord(x)
+ try:
+ if o == 0x20:
+ new_name_full += chr(0x3000)
+ elif o < 0x7F and o > 0x20:
+ new_name_full += chr(o + 0xFEE0)
+ elif o <= 0x7F:
+ self.logger.warn(f"Invalid ascii character {o:02X}")
+ return RedirectResponse("/gate/?e=4", 303)
+ else:
+ new_name_full += x
+
+ except Exception as e:
+ self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full):
+ return RedirectResponse("/gate/?e=999", 303)
+
+ return RedirectResponse("/game/mai2/?s=1", 303)
+
+ async def version_change(self, request: Request):
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ usr_sesh = UserSession()
+
+ if usr_sesh.user_id > 0:
+ form_data = await request.form()
+ maimai_version = form_data.get("version")
+ self.logger.info(f"version change to: {maimai_version}")
+ if(maimai_version.isdigit()):
+ usr_sesh.maimai_version=int(maimai_version)
+ encoded_sesh = self.encode_session(usr_sesh)
+ self.logger.info(f"Created session with JWT {encoded_sesh}")
+ resp = RedirectResponse("/game/mai2/", 303)
+ resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
+ return resp
+ else:
+ return RedirectResponse("/gate/", 303)
\ No newline at end of file
diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py
index 9aaf592..f22cccd 100644
--- a/titles/mai2/schema/item.py
+++ b/titles/mai2/schema/item.py
@@ -134,6 +134,20 @@ favorite = Table(
mysql_charset="utf8mb4",
)
+fav_music = Table(
+ "mai2_item_favorite_music",
+ metadata,
+ Column("id", Integer, primary_key=True, nullable=False),
+ Column(
+ "user",
+ ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
+ nullable=False,
+ ),
+ Column("musicId", Integer, nullable=False),
+ UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"),
+ mysql_charset="utf8mb4",
+)
+
charge = Table(
"mai2_item_charge",
metadata,
@@ -451,6 +465,30 @@ class Mai2ItemData(BaseData):
return None
return result.fetchall()
+ async def get_fav_music(self, user_id: int) -> Optional[List[Row]]:
+ result = await self.execute(fav_music.select(fav_music.c.user == user_id))
+ if result:
+ return result.fetchall()
+
+ async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]:
+ sql = insert(fav_music).values(
+ user = user_id,
+ musicId = music_id
+ )
+
+ conflict = sql.on_duplicate_key_do_nothing()
+
+ result = await self.execute(conflict)
+ if result:
+ return result.lastrowid
+
+ self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!")
+
+ async def remove_fav_music(self, user_id: int, music_id: int) -> None:
+ result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id)))
+ if not result:
+ self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!")
+
async def put_card(
self,
user_id: int,
diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py
index 8f1d5f3..1b76b07 100644
--- a/titles/mai2/schema/profile.py
+++ b/titles/mai2/schema/profile.py
@@ -491,8 +491,31 @@ consec_logins = Table(
mysql_charset="utf8mb4",
)
+rival = Table(
+ "mai2_user_rival",
+ metadata,
+ Column("id", Integer, primary_key=True, nullable=False),
+ Column(
+ "user",
+ ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
+ nullable=False,
+ ),
+ Column(
+ "rival",
+ ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
+ nullable=False,
+ ),
+ Column("show", Boolean, nullable=False, server_default="0"),
+ UniqueConstraint("user", "rival", name="mai2_user_rival_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))
+ if result:
+ return result.fetchall()
+
async def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
) -> Optional[Row]:
@@ -843,3 +866,52 @@ class Mai2ProfileData(BaseData):
if result is None:
return None
return result.fetchone()
+
+ async def get_rivals(self, user_id: int) -> Optional[List[Row]]:
+ result = await self.execute(rival.select(rival.c.user == user_id))
+ if result:
+ return result.fetchall()
+
+ async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]:
+ result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3))
+ if result:
+ return result.fetchall()
+
+ async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None:
+ sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values(
+ show = is_shown
+ )
+
+ result = await self.execute(sql)
+ if not result:
+ self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}")
+
+ async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]:
+ sql = insert(rival).values(
+ user = user_id,
+ rival = rival_id
+ )
+
+ conflict = sql.on_duplicate_key_do_nothing()
+
+ result = await self.execute(conflict)
+ if result:
+ return result.lastrowid
+
+ self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!")
+
+ async def remove_rival(self, user_id: int, rival_id: int) -> None:
+ result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id)))
+ if not result:
+ self.logger.error(f"Failed to remove rival {rival_id} for user {user_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
+ )
+ result = await self.execute(sql)
+
+ if result is None:
+ self.logger.warning(f"Failed to set user {user_id} name to {new_name}")
+ return False
+ return True
diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py
index d13faae..d4ea5b9 100644
--- a/titles/mai2/schema/score.py
+++ b/titles/mai2/schema/score.py
@@ -319,16 +319,16 @@ class Mai2ScoreData(BaseData):
sql = best_score.select(
and_(
best_score.c.user == user_id,
- (best_score.c.song_id == song_id) if song_id is not None else True,
+ (best_score.c.musicId == song_id) if song_id is not None else True,
)
- )
+ ).order_by(best_score.c.musicId).order_by(best_score.c.level)
else:
sql = best_score_old.select(
and_(
best_score_old.c.user == user_id,
- (best_score_old.c.song_id == song_id) if song_id is not None else True,
+ (best_score_old.c.musicId == song_id) if song_id is not None else True,
)
- )
+ ).order_by(best_score.c.musicId).order_by(best_score.c.level)
result = await self.execute(sql)
if result is None:
@@ -398,3 +398,23 @@ class Mai2ScoreData(BaseData):
if result is None:
return None
return result.fetchall()
+
+ async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]:
+ sql = playlog.select(playlog.c.user == user_id)
+
+ if limit:
+ sql = sql.limit(limit)
+ if idx:
+ sql = sql.offset(idx * limit)
+
+ result = await self.execute(sql)
+ if result:
+ return result.fetchall()
+
+ async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]:
+ sql = select(func.count()).where(playlog.c.user == aime_id)
+ result = await self.execute(sql)
+ if result is None:
+ self.logger.warning(f"aime_id {aime_id} has no playlog ")
+ return None
+ return result.scalar()
diff --git a/titles/mai2/templates/css/mai2_style.css b/titles/mai2/templates/css/mai2_style.css
new file mode 100644
index 0000000..4aceab8
--- /dev/null
+++ b/titles/mai2/templates/css/mai2_style.css
@@ -0,0 +1,195 @@
+.mai2-header {
+ text-align: center;
+}
+
+ul.mai2-navi {
+ list-style-type: none;
+ padding: 0;
+ overflow: hidden;
+ background-color: #333;
+ text-align: center;
+ display: inline-block;
+}
+
+ul.mai2-navi li {
+ display: inline-block;
+}
+
+ul.mai2-navi li a {
+ display: block;
+ color: white;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+}
+
+ul.mai2-navi li a:hover:not(.active) {
+ background-color: #111;
+}
+
+ul.mai2-navi li a.active {
+ background-color: #4CAF50;
+}
+
+ul.mai2-navi li.right {
+ float: right;
+}
+
+@media screen and (max-width: 600px) {
+
+ ul.mai2-navi li.right,
+ ul.mai2-navi li {
+ float: none;
+ display: block;
+ text-align: center;
+ }
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ border-collapse: separate;
+ overflow: hidden;
+ background-color: #555555;
+
+}
+
+th, td {
+ text-align: left;
+ border: none;
+
+}
+
+th {
+ color: white;
+}
+
+.table-rowdistinct tr:nth-child(even) {
+ background-color: #303030;
+}
+
+.table-rowdistinct tr:nth-child(odd) {
+ background-color: #555555;
+}
+
+caption {
+ text-align: center;
+ color: white;
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.table-large {
+ margin: 16px;
+}
+
+.table-large th,
+.table-large td {
+ padding: 8px;
+}
+
+.table-small {
+ width: 100%;
+ margin: 4px;
+}
+
+.table-small th,
+.table-small td {
+ padding: 2px;
+}
+
+.bg-card {
+ background-color: #555555;
+}
+
+.card-hover {
+ transition: all 0.2s ease-in-out;
+}
+
+.card-hover:hover {
+ transform: scale(1.02);
+}
+
+.basic {
+ color: #28a745;
+ font-weight: bold;
+}
+
+.hard {
+ color: #ffc107;
+
+ font-weight: bold;
+}
+
+.expert {
+ color: #dc3545;
+ font-weight: bold;
+}
+
+.master {
+ color: #dd09e8;
+ font-weight: bold;
+}
+
+.ultimate {
+ color: #000000;
+ font-weight: bold;
+}
+
+.score {
+ color: #ffffff;
+ font-weight: bold;
+}
+
+.rainbow {
+ background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: bold;
+}
+
+.platinum {
+ color: #FFFF00;
+ font-weight: bold;
+}
+
+.gold {
+ color: #FFFF00;
+ font-weight: bold;
+}
+
+.scrolling-text {
+ overflow: hidden;
+}
+
+.scrolling-text p {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling-text h6 {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling-text h5 {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling {
+ animation: scroll 10s linear infinite;
+}
+
+@keyframes scroll {
+ 0% {
+ transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
\ No newline at end of file
diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja
new file mode 100644
index 0000000..4a4cb86
--- /dev/null
+++ b/titles/mai2/templates/mai2_header.jinja
@@ -0,0 +1,17 @@
+
+ {% if error is defined %}
+ {% include "core/templates/widgets/err_banner.jinja" %}
+ {% endif %}
+ {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
+ No profile information found for this account.
+ {% else %}
+ Login to view profile information.
+ {% endif %}
+
+
+
+
+
+
Name change
+
+
+
+
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/titles/mai2/templates/mai2_playlog.jinja b/titles/mai2/templates/mai2_playlog.jinja
new file mode 100644
index 0000000..3e1d5fd
--- /dev/null
+++ b/titles/mai2/templates/mai2_playlog.jinja
@@ -0,0 +1,225 @@
+{% extends "core/templates/index.jinja" %}
+{% block content %}
+
+
+ {% include 'titles/mai2/templates/mai2_header.jinja' %}
+ {% if playlog is defined and playlog is not none %}
+
+
Playlog counts: {{ playlog_count }}
+ {% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
+ {% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %}
+ {% for record in playlog %}
+
+ {% set playlog_pages = playlog_count // 20 + 1 %}
+ {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
+ No Playlog information found for this account.
+ {% else %}
+ Login to view profile information.
+ {% endif %}
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py
index 00353d3..8ed0d88 100644
--- a/titles/mai2/universe.py
+++ b/titles/mai2/universe.py
@@ -1,8 +1,6 @@
from typing import Any, List, Dict
from random import randint
from datetime import datetime, timedelta
-import pytz
-import json
from core.config import CoreConfig
from titles.mai2.splashplus import Mai2SplashPlus
@@ -14,207 +12,3 @@ class Mai2Universe(Mai2SplashPlus):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_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)
- if p is None:
- return {}
-
- return {
- "userName": p["userName"],
- "rating": p["playerRating"],
- # hardcode lastDataVersion for CardMaker
- "lastDataVersion": "1.20.00",
- # checks if the user is still logged in
- "isLogin": False,
- "isExistSellingCard": True,
- }
-
- async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
- # user already exists, because the preview checks that already
- p = await self.data.profile.get_profile_detail(data["userId"], self.version)
-
- cards = await self.data.card.get_user_cards(data["userId"])
- if cards is None or len(cards) == 0:
- # This should never happen
- self.logger.error(
- f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
- )
- return {}
-
- # get the dict representation of the row so we can modify values
- user_data = p._asdict()
-
- # remove the values the game doesn't want
- user_data.pop("id")
- user_data.pop("user")
- user_data.pop("version")
-
- return {"userId": data["userId"], "userData": user_data}
-
- async def handle_cm_login_api_request(self, data: Dict) -> Dict:
- return {"returnCode": 1}
-
- async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
- return {"returnCode": 1}
-
- async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
- selling_cards = await self.data.static.get_enabled_cards(self.version)
- if selling_cards is None:
- return {"length": 0, "sellingCardList": []}
-
- selling_card_list = []
- for card in selling_cards:
- tmp = card._asdict()
- tmp.pop("id")
- tmp.pop("version")
- tmp.pop("cardName")
- tmp.pop("enabled")
-
- tmp["startDate"] = datetime.strftime(
- tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
- )
- tmp["endDate"] = datetime.strftime(
- tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
- )
- tmp["noticeStartDate"] = datetime.strftime(
- tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
- )
- tmp["noticeEndDate"] = datetime.strftime(
- tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
- )
-
- selling_card_list.append(tmp)
-
- return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
-
- async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
- user_cards = await self.data.item.get_cards(data["userId"])
- if user_cards is None:
- return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
-
- max_ct = data["maxCount"]
- next_idx = data["nextIndex"]
- start_idx = next_idx
- end_idx = max_ct + start_idx
-
- if len(user_cards[start_idx:]) > max_ct:
- next_idx += max_ct
- else:
- next_idx = 0
-
- card_list = []
- for card in user_cards:
- tmp = card._asdict()
- tmp.pop("id")
- tmp.pop("user")
-
- tmp["startDate"] = datetime.strftime(
- tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
- )
- tmp["endDate"] = datetime.strftime(
- tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
- )
- card_list.append(tmp)
-
- return {
- "returnCode": 1,
- "length": len(card_list[start_idx:end_idx]),
- "nextIndex": next_idx,
- "userCardList": card_list[start_idx:end_idx],
- }
-
- async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
- await super().handle_get_user_item_api_request(data)
-
- async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
- characters = await self.data.item.get_characters(data["userId"])
-
- chara_list = []
- for chara in characters:
- chara_list.append(
- {
- "characterId": chara["characterId"],
- # no clue why those values are even needed
- "point": 0,
- "count": 0,
- "level": chara["level"],
- "nextAwake": 0,
- "nextAwakePercent": 0,
- "favorite": False,
- "awakening": chara["awakening"],
- "useCount": chara["useCount"],
- }
- )
-
- return {
- "returnCode": 1,
- "length": len(chara_list),
- "userCharacterList": chara_list,
- }
-
- async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
- return {"length": 0, "userPrintDetailList": []}
-
- async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
- user_id = data["userId"]
- upsert = data["userPrintDetail"]
-
- # set a random card serial number
- serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
-
- # calculate start and end date of the card
- start_date = datetime.utcnow()
- end_date = datetime.utcnow() + timedelta(days=15)
-
- user_card = upsert["userCard"]
- await self.data.item.put_card(
- user_id,
- user_card["cardId"],
- user_card["cardTypeId"],
- user_card["charaId"],
- user_card["mapId"],
- # add the correct start date and also the end date in 15 days
- start_date,
- end_date,
- )
-
- # get the profile extend to save the new bought card
- extend = await self.data.profile.get_profile_extend(user_id, self.version)
- if extend:
- extend = extend._asdict()
- # parse the selectedCardList
- # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
- selected_cards: List = extend["selectedCardList"]
-
- # if no pass is already added, add the corresponding pass
- if not user_card["cardTypeId"] in selected_cards:
- selected_cards.insert(0, user_card["cardTypeId"])
-
- extend["selectedCardList"] = selected_cards
- await self.data.profile.put_profile_extend(user_id, self.version, extend)
-
- # properly format userPrintDetail for the database
- upsert.pop("userCard")
- upsert.pop("serialId")
- upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
-
- await self.data.item.put_user_print_detail(user_id, serial_id, upsert)
-
- return {
- "returnCode": 1,
- "orderId": 0,
- "serialId": serial_id,
- "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
- "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
- }
-
- async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
- return {
- "returnCode": 1,
- "orderId": 0,
- "serialId": data["userPrintlog"]["serialId"],
- }
-
- async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
- return {"returnCode": 1}
diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py
index 1cdec03..226f318 100644
--- a/titles/ongeki/frontend.py
+++ b/titles/ongeki/frontend.py
@@ -31,7 +31,8 @@ class OngekiFrontend(FE_Base):
def get_routes(self) -> List[Route]:
return [
- Route("/", self.render_GET)
+ Route("/", self.render_GET),
+ Route("/version.change", self.render_POST, methods=['POST'])
]
async def render_GET(self, request: Request) -> bytes:
@@ -69,29 +70,34 @@ class OngekiFrontend(FE_Base):
return RedirectResponse("/gate/", 303)
async def render_POST(self, request: Request):
- uri = request.uri.decode()
+ uri = request.url.path
+ frm = await request.form()
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if uri == "/game/ongeki/rival.add":
- rival_id = request.args[b"rivalUserId"][0].decode()
+ rival_id = frm.get("rivalUserId")
await self.data.profile.put_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{usr_sesh.user_id} added a rival")
return RedirectResponse(b"/game/ongeki/", 303)
elif uri == "/game/ongeki/rival.delete":
- rival_id = request.args[b"rivalUserId"][0].decode()
+ rival_id = frm.get("rivalUserId")
await self.data.profile.delete_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{response}")
return RedirectResponse(b"/game/ongeki/", 303)
elif uri == "/game/ongeki/version.change":
- ongeki_version=request.args[b"version"][0].decode()
+ ongeki_version=frm.get("version")
if(ongeki_version.isdigit()):
usr_sesh.ongeki_version=int(ongeki_version)
- return RedirectResponse("/game/ongeki/", 303)
+ enc = self.encode_session(usr_sesh)
+ resp = RedirectResponse("/game/ongeki/", 303)
+ resp.delete_cookie('ARTEMIS_SESH')
+ resp.set_cookie('ARTEMIS_SESH', enc)
+ return resp
else:
Response("Something went wrong", status_code=500)
diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py
index f5ab4e1..6867133 100644
--- a/titles/ongeki/schema/score.py
+++ b/titles/ongeki/schema/score.py
@@ -30,7 +30,7 @@ score_best = Table(
Column("isFullCombo", Boolean, nullable=False),
Column("isAllBreake", Boolean, nullable=False),
Column("isLock", Boolean, nullable=False),
- Column("clearStatus", Boolean, nullable=False),
+ Column("clearStatus", Integer, nullable=False),
Column("isStoryWatched", Boolean, nullable=False),
Column("platinumScoreMax", Integer),
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),