Merge pull request '[chunithm] Support LUMINOUS' (#154) from beerpsi/artemis:feat/chunithm/luminous into develop

Reviewed-on: Hay1tsme/artemis#154
This commit is contained in:
Hay1tsme 2024-06-20 18:37:12 +00:00
commit 3741c286f8
10 changed files with 493 additions and 9 deletions

View File

@ -1,6 +1,10 @@
# 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.
## 20240620
### CHUNITHM
+ CHUNITHM LUMINOUS support
## 20240530 ## 20240530
### DIVA ### DIVA
+ Fix reader for when dificulty is not a int + Fix reader for when dificulty is not a int

View File

@ -0,0 +1,87 @@
"""CHUNITHM LUMINOUS
Revision ID: b23f985100ba
Revises: 3657efefc5a4
Create Date: 2024-06-20 08:08:08.759261
"""
from alembic import op
from sqlalchemy import Column, Integer, Boolean, UniqueConstraint
# revision identifiers, used by Alembic.
revision = 'b23f985100ba'
down_revision = '3657efefc5a4'
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

@ -63,6 +63,7 @@ Games listed below have been tested and confirmed working.
| 12 | CHUNITHM NEW PLUS!! | | 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN | | 13 | CHUNITHM SUN |
| 14 | CHUNITHM SUN PLUS | | 14 | CHUNITHM SUN PLUS |
| 15 | CHUNITHM LUMINOUS |
### Importer ### Importer

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

@ -941,6 +941,31 @@ class ChuniBase:
rating_type, rating_type,
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"}
@ -969,4 +994,4 @@ class ChuniBase:
return { return {
"userId": data["userId"], "userId": data["userId"],
"userNetBattleData": {"recentNBSelectMusicList": []}, "userNetBattleData": {"recentNBSelectMusicList": []},
} }

View File

@ -1,3 +1,6 @@
from enum import Enum
class ChuniConstants: class ChuniConstants:
GAME_CODE = "SDBT" GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD" GAME_CODE_NEW = "SDHD"
@ -20,6 +23,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,9 +39,21 @@ 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
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver] return cls.VERSION_NAMES[ver]
class MapAreaConditionType(Enum):
UNLOCKED = "0"
MAP_AREA_CLEARED = "2"
TROPHY_OBTAINED = "3"
class MapAreaConditionLogicalOperator(Enum):
OR = "0"
AND = "1"

View File

@ -1,7 +1,8 @@
from starlette.requests import Request from starlette.requests import Request
from starlette.routing import Route from starlette.routing import Route
from starlette.responses import Response from starlette.responses import Response
import logging, coloredlogs import logging
import coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
import zlib import zlib
import yaml import yaml
@ -34,6 +35,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:
@ -61,6 +63,7 @@ class ChuniServlet(BaseServlet):
ChuniNewPlus, ChuniNewPlus,
ChuniSun, ChuniSun,
ChuniSunPlus, ChuniSunPlus,
ChuniLuminous,
] ]
self.logger = logging.getLogger("chuni") self.logger = logging.getLogger("chuni")
@ -103,7 +106,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
@ -195,8 +200,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 PLUS 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: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif game_code == "SDGS": # Int elif game_code == "SDGS": # Int
if version < 110: # SUPERSTAR / SUPERSTAR PLUS if version < 110: # SUPERSTAR / SUPERSTAR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it
@ -206,8 +213,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: # LUMINOUS
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
@ -295,7 +304,7 @@ class ChuniServlet(BaseServlet):
self.logger.error(f"Error handling v{version} method {endpoint} - {e}") self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return Response(zlib.compress(b'{"stat": "0"}')) return Response(zlib.compress(b'{"stat": "0"}'))
if resp == None: if resp is None:
resp = {"returnCode": 1} resp = {"returnCode": 1}
self.logger.debug(f"Response {resp}") self.logger.debug(f"Response {resp}")
@ -313,4 +322,4 @@ class ChuniServlet(BaseServlet):
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
) )
return Response(crypt.encrypt(padded)) return Response(crypt.encrypt(padded))

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

@ -0,0 +1,244 @@
from typing import Dict
from core.config import CoreConfig
from titles.chuni.sunplus import ChuniSunPlus
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
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_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = await super().handle_cm_get_user_preview_api_request(data)
# Does CARD MAKER 1.35 work this far up?
user_data["lastDataVersion"] = "2.20.00"
return user_data
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,
}
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
# There is no game data for this, everything is server side.
# TODO: Figure out conditions for 1UM1N0US ep.111
return {
"length": "7",
"gameMapAreaConditionList": [
# Secret AREA: MUSIC GAME
{
"mapAreaId": "2206201", # BlythE ULTIMA
"length": "1",
# Obtain the trophy "MISSION in progress", which is only available
# when running the first CHUNITHM mission
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6832",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 02:00:00.0",
}
],
},
{
"mapAreaId": "2206202", # PRIVATE SERVICE ULTIMA
"length": "1",
# Obtain the trophy "MISSION in progress", which is only available
# when running the first CHUNITHM mission
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6832",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 02:00:00.0",
}
],
},
{
"mapAreaId": "2206203", # New York Back Raise
"length": "1",
# SS NightTheater's EXPERT chart and get the title
# "今宵、劇場に映し出される景色とは――――。"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6833",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": "2206204", # Spasmodic
"length": "2",
# - Get 1 miss on Random (any difficulty) and get the title "当たり待ち"
# - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6834",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6835",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": "2206205", # ΩΩPARTS
"length": "2",
# - S Sage EXPERT to get the title "マターリ進行キボンヌ"
# - Equip this title and play cab-to-cab with another person with this title
# to get "マターリしようよ"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6836",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6837",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2024-01-25 02:00:00.0",
},
],
},
{
"mapAreaId": "2206206", # Blow My Mind
"length": "1",
# SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT
# to get the title "Can you hear me?"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": "6838",
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": "2206207", # VALLIS-NERIA
"length": "6",
# Finish the 6 other areas
"mapAreaConditionList": [
{
"type": MapAreaConditionType.MAP_AREA_CLEARED.value,
"conditionId": str(x),
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": "2023-12-14 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
}
for x in range(2206201, 2206207)
],
},
# {
# "mapAreaId": "3229301", # Mystic Rainbow of LUMINOUS Area 1
# "length": "1",
# # Unlocks when any of the mainline LUMINOUS maps are completed?
# "mapAreaConditionList": [
# # TODO
# ]
# },
# {
# "mapAreaId": "3229302", # Mystic Rainbow of LUMINOUS Area 2
# "length": "5",
# # Unlocks when LUMINOUS ep. I is completed
# "mapAreaConditionList": [
# {
# "type": MapAreaConditionType.MAP_AREA_CLEARED.value,
# "conditionId": str(x),
# "logicalOpe": MapAreaConditionLogicalOperator.AND.value,
# "startDate": "2023-12-14 07:00:00.0",
# "endDate": "2099-12-31 00:00:00.0",
# }
# for x in range(3220101, 3220106)
# ]
# },
# {
# "mapAreaId": "3229303", # Mystic Rainbow of LUMINOUS Area 3
# "length": "5",
# # Unlocks when LUMINOUS ep. II is completed
# "mapAreaConditionList": [
# {
# "type": MapAreaConditionType.MAP_AREA_CLEARED.value,
# "conditionId": str(x),
# "logicalOpe": MapAreaConditionLogicalOperator.AND.value,
# "startDate": "2023-12-14 07:00:00.0",
# "endDate": "2099-12-31 00:00:00.0",
# }
# for x in range(3220201, 3220206)
# ]
# },
# {
# "mapAreaId": "3229304", # Mystic Rainbow of LUMINOUS Area 4
# "length": "5",
# # Unlocks when LUMINOUS ep. III is completed
# "mapAreaConditionList": [
# ]
# }
],
}

View File

@ -32,6 +32,8 @@ class ChuniNew(ChuniBase):
return "210" return "210"
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
return "215" return "215"
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
return "220"
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9 # use UTC time and convert it to JST time by adding +9

View File

@ -243,6 +243,36 @@ 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]:
@ -594,3 +624,66 @@ 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()