Support CHUNITHM LUMINOUS

This commit is contained in:
2024-04-20 14:07:37 +00:00
parent f4211e741a
commit a2a1a578a4
8 changed files with 511 additions and 6 deletions

View File

@ -0,0 +1,89 @@
"""chunithm luminous
Revision ID: c143b80bd966
Revises: 6a7e8277763b
Create Date: 2024-04-20 14:06:54.630558
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy import Column, UniqueConstraint
from sqlalchemy.types import Integer, Boolean
# revision identifiers, used by Alembic.
revision = 'c143b80bd966'
down_revision = '6a7e8277763b'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"chuni_profile_net_battle",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("isRankUpChallengeFailed", Boolean),
Column("highestBattleRankId", Integer),
Column("battleIconId", Integer),
Column("battleIconNum", Integer),
Column("avatarEffectPoint", Integer),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_profile_net_battle",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
op.create_table(
"chuni_item_cmission",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("missionId", Integer, nullable=False),
Column("point", Integer),
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_item_cmission",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
op.create_table(
"chuni_item_cmission_progress",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("missionId", Integer, nullable=False),
Column("order", Integer),
Column("stage", Integer),
Column("progress", Integer),
UniqueConstraint(
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_item_cmission_progress",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
def downgrade():
op.drop_table("chuni_profile_net_battle")
op.drop_table("chuni_item_cmission")
op.drop_table("chuni_item_cmission_progress")

View File

@ -22,6 +22,9 @@ version:
14: 14:
rom: 2.15.00 rom: 2.15.00
data: 2.15.00 data: 2.15.00
15:
rom: 2.20.00
data: 2.20.00
crypto: crypto:
encrypted_only: False encrypted_only: False

View File

@ -938,6 +938,31 @@ class ChuniBase:
upsert[rating_type], upsert[rating_type],
) )
# added in LUMINOUS
if "userCMissionList" in upsert:
for cmission in upsert["userCMissionList"]:
mission_id = cmission["missionId"]
await self.data.item.put_cmission(
user_id,
{
"missionId": mission_id,
"point": cmission["point"],
},
)
for progress in cmission["userCMissionProgressList"]:
await self.data.item.put_cmission_progress(user_id, mission_id, progress)
if "userNetBattleData" in upsert:
net_battle = upsert["userNetBattleData"][0]
# fix the boolean
net_battle["isRankUpChallengeFailed"] = (
False if net_battle["isRankUpChallengeFailed"] == "false" else True
)
await self.data.profile.put_net_battle(user_id, net_battle)
return {"returnCode": "1"} return {"returnCode": "1"}
async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:

View File

@ -20,6 +20,7 @@ class ChuniConstants:
VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN = 13
VER_CHUNITHM_SUN_PLUS = 14 VER_CHUNITHM_SUN_PLUS = 14
VER_CHUNITHM_LUMINOUS = 15
VERSION_NAMES = [ VERSION_NAMES = [
"CHUNITHM", "CHUNITHM",
"CHUNITHM PLUS", "CHUNITHM PLUS",
@ -35,7 +36,8 @@ class ChuniConstants:
"CHUNITHM NEW!!", "CHUNITHM NEW!!",
"CHUNITHM NEW PLUS!!", "CHUNITHM NEW PLUS!!",
"CHUNITHM SUN", "CHUNITHM SUN",
"CHUNITHM SUN PLUS" "CHUNITHM SUN PLUS",
"CHUNITHM_LUMINOUS",
] ]
@classmethod @classmethod

View File

@ -32,6 +32,7 @@ from .new import ChuniNew
from .newplus import ChuniNewPlus from .newplus import ChuniNewPlus
from .sun import ChuniSun from .sun import ChuniSun
from .sunplus import ChuniSunPlus from .sunplus import ChuniSunPlus
from .luminous import ChuniLuminous
class ChuniServlet(BaseServlet): class ChuniServlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@ -59,6 +60,7 @@ class ChuniServlet(BaseServlet):
ChuniNewPlus, ChuniNewPlus,
ChuniSun, ChuniSun,
ChuniSunPlus, ChuniSunPlus,
ChuniLuminous,
] ]
self.logger = logger.create_logger( self.logger = logger.create_logger(
@ -81,7 +83,9 @@ class ChuniServlet(BaseServlet):
for method in method_list: for method in method_list:
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN and then to 36 # number of iterations was changed to 70 in SUN and then to 36
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: if version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
iter_count = 8
elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
iter_count = 36 iter_count = 36
elif version == ChuniConstants.VER_CHUNITHM_SUN: elif version == ChuniConstants.VER_CHUNITHM_SUN:
iter_count = 70 iter_count = 70
@ -173,8 +177,10 @@ class ChuniServlet(BaseServlet):
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210 and version < 215: # SUN elif version >= 210 and version < 215: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 215: # SUN elif version >= 215 and version < 220: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 220:
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif game_code == "SDGS": # Int elif game_code == "SDGS": # Int
if version < 110: # SUPERSTAR if version < 110: # SUPERSTAR
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE" internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE"
@ -184,8 +190,10 @@ class ChuniServlet(BaseServlet):
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 120 and version < 125: # SUN elif version >= 120 and version < 125: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 125: # SUN PLUS elif version >= 125 and version < 130: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 130:
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
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

259
titles/chuni/luminous.py Normal file
View File

@ -0,0 +1,259 @@
from typing import Dict
from core.config import CoreConfig
from core.utils import Utils
from titles.chuni.sunplus import ChuniSunPlus
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniLuminous(ChuniSunPlus):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = await super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
"rom"
]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
"data"
]
t_port = ""
if (
not self.core_cfg.server.is_using_proxy
and Utils.get_title_port(self.core_cfg) != 80
):
t_port = f":{self.core_cfg.server.port}"
ret["gameSetting"][
"matchingUri"
] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/"
ret["gameSetting"][
"matchingUriX"
] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/"
ret["gameSetting"][
"udpHolePunchUri"
] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/"
ret["gameSetting"][
"reflectorUri"
] = f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/220/ChuniServlet/"
return ret
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)
# I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up
user_data["lastDataVersion"] = "2.20.00"
return user_data
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
return {
"length": 1,
"gameMapAreaConditionList": [
{
"mapAreaId": "2206201", # BlythE ULTIMA
"length": 1,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6832", # MISSION in progress
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 00:00:00.0",
},
],
},
{
"mapAreaId": "2206202", # PRIVATE SERVICE ULTIMA
"length": 1,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6832", # MISSION in progress
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 00:00:00.0",
},
],
},
{
"mapAreaId": "2206203", # New York Back Raise
"length": 1,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6833", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": "2206204", # Spasmodic
"length": 2,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6834", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6835", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": "2206205", # ΩΩPARTS
"length": 2,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6836", # マターリ進行キボンヌ
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6837", # マターリしようよ
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 00:00:00.0",
},
],
},
{
"mapAreaId": "2206206", # Blow My Mind
"length": 1,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6838", # Can you hear me?
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
# TODO: Proper VALLIS-NERIA: Unlock other areas
# {
# "mapAreaId": "2206207",
# "length": 0,
# "mapAreaConditionList": [
# ],
# }
{
"mapAreaId": "2206207",
"length": 7,
"mapAreaConditionList": [
{
"type": "3",
"conditionId": "6832", # MISSION in progress
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 00:00:00.0",
},
{
"type": "3",
"conditionId": "6833", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6834", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6835", # 今宵、劇場に映し出される景色とは――――。
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6836", # マターリ進行キボンヌ
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": "3",
"conditionId": "6837", # マターリしようよ
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 00:00:00.0",
},
{
"type": "3",
"conditionId": "6838", # Can you hear me?
"logicalOpe": "1",
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
}
]
}
async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
mission_id = data["missionId"]
progress_list = []
point = 0
mission_data = await self.data.item.get_cmission(user_id, mission_id)
progress_data = await self.data.item.get_cmission_progress(user_id, mission_id)
if mission_data and progress_data:
point = mission_data["point"]
for progress in progress_data:
progress_list.append(
{
"order": progress["order"],
"stage": progress["stage"],
"progress": progress["progress"],
}
)
return {
"userId": user_id,
"missionId": mission_id,
"point": point,
"userCMissionProgressList": progress_list,
}
async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
net_battle = {}
net_battle_data = await self.data.profile.get_net_battle(user_id)
if net_battle_data:
net_battle = {
"isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"],
"highestBattleRankId": net_battle_data["highestBattleRankId"],
"battleIconId": net_battle_data["battleIconId"],
"battleIconNum": net_battle_data["battleIconNum"],
"avatarEffectPoint": net_battle_data["avatarEffectPoint"],
}
return {
"userId": user_id,
"userNetBattleData": net_battle,
}

View File

@ -242,13 +242,43 @@ matching = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
cmission = Table(
"chuni_item_cmission",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("missionId", Integer, nullable=False),
Column("point", Integer),
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
mysql_charset="utf8mb4",
)
cmission_progress = Table(
"chuni_item_cmission_progress",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("missionId", Integer, nullable=False),
Column("order", Integer),
Column("stage", Integer),
Column("progress", Integer),
UniqueConstraint(
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
),
mysql_charset="utf8mb4",
)
class ChuniItemData(BaseData): class ChuniItemData(BaseData):
async def get_oldest_free_matching(self, version: int) -> Optional[Row]: async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
sql = matching.select( sql = matching.select(
and_( and_(
matching.c.version == version, matching.c.version == version,
matching.c.isFull == False matching.c.isFull == False # noqa: E712
) )
).order_by(matching.c.roomId.asc()) ).order_by(matching.c.roomId.asc())
@ -593,3 +623,61 @@ class ChuniItemData(BaseData):
) )
return None return None
return result.lastrowid return result.lastrowid
async def put_cmission_progress(
self, user_id: int, mission_id: int, progress_data: Dict
) -> Optional[int]:
progress_data["user"] = user_id
progress_data["missionId"] = mission_id
sql = insert(cmission_progress).values(**progress_data)
conflict = sql.on_duplicate_key_update(**progress_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_cmission_progress(
self, user_id: int, mission_id: int
) -> Optional[List[Row]]:
sql = cmission_progress.select(
and_(
cmission_progress.c.user == user_id,
cmission_progress.c.missionId == mission_id,
)
).order_by(cmission_progress.c.order.asc())
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]:
sql = cmission.select(
and_(cmission.c.user == user_id, cmission.c.missionId == mission_id)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]:
mission_data["user"] = user_id
sql = insert(cmission).values(**mission_data)
conflict = sql.on_duplicate_key_update(**mission_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_cmissions(self, user_id: int) -> Optional[List[Row]]:
sql = cmission.select(cmission.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -412,6 +412,19 @@ rating = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
net_battle = Table(
"chuni_profile_net_battle",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("isRankUpChallengeFailed", Boolean),
Column("highestBattleRankId", Integer),
Column("battleIconId", Integer),
Column("battleIconNum", Integer),
Column("avatarEffectPoint", Integer),
mysql_charset="utf8mb4",
)
class ChuniProfileData(BaseData): class ChuniProfileData(BaseData):
async def update_name(self, user_id: int, new_name: str) -> bool: async def update_name(self, user_id: int, new_name: str) -> bool:
@ -729,7 +742,7 @@ class ChuniProfileData(BaseData):
total_play_count = 0 total_play_count = 0
for row in playcount_sql: for row in playcount_sql:
total_play_count += row[0] total_play_count = row[0]
return { return {
"total_play_count": total_play_count "total_play_count": total_play_count
} }
@ -757,3 +770,21 @@ class ChuniProfileData(BaseData):
return return
return result.lastrowid return result.lastrowid
async def get_net_battle(self, aime_id: int) -> Optional[Row]:
sql = select(net_battle).where(net_battle.c.user == aime_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_net_battle(self, aime_id: int, net_battle_data: Dict) -> Optional[int]:
net_battle_data["user"] = aime_id
sql = insert(net_battle).values(**net_battle_data)
conflict = sql.on_duplicate_key_update(**net_battle_data)
result = await self.execute(conflict)
if result is None:
return None