mirror of
https://gitea.tendokyu.moe/Hay1tsme/artemis.git
synced 2026-02-04 14:47:29 +08:00
CHUNITHM X-VERSE support (#238)
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/238 Co-authored-by: beerpsi <beerpsi@duck.com> Co-committed-by: beerpsi <beerpsi@duck.com>
This commit is contained in:
82
core/data/alembic/versions/8b57e9646449_linked_verse.py
Normal file
82
core/data/alembic/versions/8b57e9646449_linked_verse.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Linked VERSE
|
||||||
|
|
||||||
|
Revision ID: 8b57e9646449
|
||||||
|
Revises: bdf710616ba4
|
||||||
|
Create Date: 2025-12-12 16:09:07.530809
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "8b57e9646449"
|
||||||
|
down_revision = "bdf710616ba4"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"chuni_static_linked_verse",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("version", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("linkedVerseId", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("isEnabled", sa.Boolean(), server_default="1", nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"startDate", sa.TIMESTAMP(), server_default=sa.text("now()"), nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("courseId1", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("courseId2", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("courseId3", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("courseId4", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("courseId5", sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"version", "linkedVerseId", name="chuni_static_linked_verse_pk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"chuni_item_linked_verse",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("linkedVerseId", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("progress", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("statusOpen", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("statusUnlock", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("isFirstClear", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("numClear", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearCourseId", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearCourseLevel", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearScore", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearDate", sa.String(length=25), nullable=True),
|
||||||
|
sa.Column("clearUserId1", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearUserId2", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearUserId3", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("clearUserName0", sa.String(length=20), nullable=True),
|
||||||
|
sa.Column("clearUserName1", sa.String(length=20), nullable=True),
|
||||||
|
sa.Column("clearUserName2", sa.String(length=20), nullable=True),
|
||||||
|
sa.Column("clearUserName3", sa.String(length=20), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("user", "linkedVerseId", name="chuni_item_linked_verse_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"chuni_profile_data",
|
||||||
|
sa.Column("stageId", sa.Integer(), nullable=False, server_default="99999"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("chuni_profile_data", "stageId")
|
||||||
|
op.drop_table("chuni_item_linked_verse")
|
||||||
|
op.drop_table("chuni_static_linked_verse")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -69,6 +69,7 @@ Games listed below have been tested and confirmed working.
|
|||||||
| 15 | CHUNITHM LUMINOUS |
|
| 15 | CHUNITHM LUMINOUS |
|
||||||
| 16 | CHUNITHM LUMINOUS PLUS |
|
| 16 | CHUNITHM LUMINOUS PLUS |
|
||||||
| 17 | CHUNITHM VERSE |
|
| 17 | CHUNITHM VERSE |
|
||||||
|
| 18 | CHUNITHM X-VERSE |
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mods:
|
|||||||
use_login_bonus: True
|
use_login_bonus: True
|
||||||
# stock_tickets allows specified ticket IDs to be auto-stocked at login. Format is a comma-delimited string of ticket IDs
|
# stock_tickets allows specified ticket IDs to be auto-stocked at login. Format is a comma-delimited string of ticket IDs
|
||||||
# note: quanity is not refreshed on "continue" after set - only on subsequent login
|
# note: quanity is not refreshed on "continue" after set - only on subsequent login
|
||||||
stock_tickets:
|
stock_tickets:
|
||||||
stock_count: 99
|
stock_count: 99
|
||||||
|
|
||||||
# Allow use of all available customization items in frontend web ui
|
# Allow use of all available customization items in frontend web ui
|
||||||
@@ -19,12 +19,12 @@ mods:
|
|||||||
# warning: This can result in pushing a lot of data, especially the userbox items. Recommended for local network use only.
|
# warning: This can result in pushing a lot of data, especially the userbox items. Recommended for local network use only.
|
||||||
forced_item_unlocks:
|
forced_item_unlocks:
|
||||||
map_icons: False
|
map_icons: False
|
||||||
system_voices: False
|
system_voices: False
|
||||||
avatar_accessories: False
|
avatar_accessories: False
|
||||||
nameplates: False
|
nameplates: False
|
||||||
trophies: False
|
trophies: False
|
||||||
character_icons: False
|
character_icons: False
|
||||||
|
|
||||||
version:
|
version:
|
||||||
11:
|
11:
|
||||||
rom: 2.00.00
|
rom: 2.00.00
|
||||||
@@ -47,6 +47,9 @@ version:
|
|||||||
17:
|
17:
|
||||||
rom: 2.30.00
|
rom: 2.30.00
|
||||||
data: 2.30.00
|
data: 2.30.00
|
||||||
|
18:
|
||||||
|
rom: 2.40.00
|
||||||
|
data: 2.40.00
|
||||||
|
|
||||||
crypto:
|
crypto:
|
||||||
encrypted_only: False
|
encrypted_only: False
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
+ LUMINOUS
|
+ LUMINOUS
|
||||||
+ LUMINOUS PLUS
|
+ LUMINOUS PLUS
|
||||||
+ VERSE
|
+ VERSE
|
||||||
|
+ X-VERSE
|
||||||
|
|
||||||
+ crossbeats REV.
|
+ crossbeats REV.
|
||||||
+ Crossbeats REV.
|
+ Crossbeats REV.
|
||||||
|
|||||||
@@ -1096,7 +1096,7 @@ class ChuniBase:
|
|||||||
|
|
||||||
for fav_id in added_ids:
|
for fav_id in added_ids:
|
||||||
await self.data.item.put_favorite_music(user_id, self.version, fav_id)
|
await self.data.item.put_favorite_music(user_id, self.version, fav_id)
|
||||||
|
|
||||||
# added in CHUNITHM VERSE
|
# added in CHUNITHM VERSE
|
||||||
if "userUnlockChallengeList" in upsert:
|
if "userUnlockChallengeList" in upsert:
|
||||||
for unlock_challenge in upsert["userUnlockChallengeList"]:
|
for unlock_challenge in upsert["userUnlockChallengeList"]:
|
||||||
@@ -1104,6 +1104,10 @@ class ChuniBase:
|
|||||||
user_id, self.version, unlock_challenge
|
user_id, self.version, unlock_challenge
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# added in CHUNITHM X-VERSE
|
||||||
|
if "userLinkedVerseList" in upsert:
|
||||||
|
for linked_verse in upsert["userLinkedVerseList"]:
|
||||||
|
await self.data.item.put_linked_verse(user_id, linked_verse)
|
||||||
|
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class ChuniConstants:
|
|||||||
VER_CHUNITHM_LUMINOUS = 15
|
VER_CHUNITHM_LUMINOUS = 15
|
||||||
VER_CHUNITHM_LUMINOUS_PLUS = 16
|
VER_CHUNITHM_LUMINOUS_PLUS = 16
|
||||||
VER_CHUNITHM_VERSE = 17
|
VER_CHUNITHM_VERSE = 17
|
||||||
|
VER_CHUNITHM_X_VERSE = 18
|
||||||
|
|
||||||
VERSION_NAMES = [
|
VERSION_NAMES = [
|
||||||
"CHUNITHM",
|
"CHUNITHM",
|
||||||
@@ -48,7 +49,8 @@ class ChuniConstants:
|
|||||||
"CHUNITHM SUN PLUS",
|
"CHUNITHM SUN PLUS",
|
||||||
"CHUNITHM LUMINOUS",
|
"CHUNITHM LUMINOUS",
|
||||||
"CHUNITHM LUMINOUS PLUS",
|
"CHUNITHM LUMINOUS PLUS",
|
||||||
"CHUNITHM VERSE"
|
"CHUNITHM VERSE",
|
||||||
|
"CHUNITHM X-VERSE",
|
||||||
]
|
]
|
||||||
|
|
||||||
SCORE_RANK_INTERVALS_OLD = [
|
SCORE_RANK_INTERVALS_OLD = [
|
||||||
@@ -100,6 +102,8 @@ class ChuniConstants:
|
|||||||
"215": VER_CHUNITHM_SUN_PLUS,
|
"215": VER_CHUNITHM_SUN_PLUS,
|
||||||
"220": VER_CHUNITHM_LUMINOUS,
|
"220": VER_CHUNITHM_LUMINOUS,
|
||||||
"225": VER_CHUNITHM_LUMINOUS_PLUS,
|
"225": VER_CHUNITHM_LUMINOUS_PLUS,
|
||||||
|
"230": VER_CHUNITHM_VERSE,
|
||||||
|
"240": VER_CHUNITHM_X_VERSE,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -246,6 +250,44 @@ class MapAreaConditionType(IntEnum):
|
|||||||
"""Obtain a rating of at least `conditionId / 100`."""
|
"""Obtain a rating of at least `conditionId / 100`."""
|
||||||
|
|
||||||
|
|
||||||
|
class LinkedVerseUnlockConditionType(IntEnum):
|
||||||
|
"""
|
||||||
|
`conditionList` is a semicolon-delimited list of numbers, where the number's meaning
|
||||||
|
is defined by the specific `conditionId`. Additionally, each element of the list
|
||||||
|
can be further separated by underscores. For example `1;2_3;4` means that the player
|
||||||
|
must achieve 1 AND (2 OR 3) AND 4.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLAY_SONGS = 33
|
||||||
|
"""
|
||||||
|
Play songs given by `conditionList`, where `conditionList` is a
|
||||||
|
list of song IDs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
COURSE_CLEAR_AND_CLASS_EMBLEM = 34
|
||||||
|
"""
|
||||||
|
Obtain a class emblem (by clearing all courses of a given class) on **any**
|
||||||
|
of the classes given by `conditionList`, where `conditionList` is an
|
||||||
|
underscore-separated list of class IDs (1 for CLASS I to 6 for CLASS ∞).
|
||||||
|
"""
|
||||||
|
|
||||||
|
TROPHY_OBTAINED = 35
|
||||||
|
"""
|
||||||
|
Obtain trophies given by `conditionList`, where `conditionList` is a
|
||||||
|
list of trophy IDs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLAY_SONGS_IN_FAVORITE = 36
|
||||||
|
"""
|
||||||
|
Play songs given by `conditionList` **from the favorites folder**, where
|
||||||
|
`conditionList` is a list of song IDs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CLEAR_TEAM_COURSE_WITH_CHARACTER_OF_MINIMUM_RANK = 37
|
||||||
|
"""
|
||||||
|
Clear a team course while equipping a character of minimum rank.
|
||||||
|
"""
|
||||||
|
|
||||||
class MapAreaConditionLogicalOperator(Enum):
|
class MapAreaConditionLogicalOperator(Enum):
|
||||||
AND = 1
|
AND = 1
|
||||||
OR = 2
|
OR = 2
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from .sunplus import ChuniSunPlus
|
|||||||
from .luminous import ChuniLuminous
|
from .luminous import ChuniLuminous
|
||||||
from .luminousplus import ChuniLuminousPlus
|
from .luminousplus import ChuniLuminousPlus
|
||||||
from .verse import ChuniVerse
|
from .verse import ChuniVerse
|
||||||
|
from .xverse import ChuniXVerse
|
||||||
|
|
||||||
|
|
||||||
class ChuniServlet(BaseServlet):
|
class ChuniServlet(BaseServlet):
|
||||||
@@ -70,7 +71,8 @@ class ChuniServlet(BaseServlet):
|
|||||||
ChuniSunPlus,
|
ChuniSunPlus,
|
||||||
ChuniLuminous,
|
ChuniLuminous,
|
||||||
ChuniLuminousPlus,
|
ChuniLuminousPlus,
|
||||||
ChuniVerse
|
ChuniVerse,
|
||||||
|
ChuniXVerse,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger = logging.getLogger("chuni")
|
self.logger = logging.getLogger("chuni")
|
||||||
@@ -119,6 +121,7 @@ class ChuniServlet(BaseServlet):
|
|||||||
f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8,
|
f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8,
|
||||||
ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56,
|
ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56,
|
||||||
ChuniConstants.VER_CHUNITHM_VERSE: 42,
|
ChuniConstants.VER_CHUNITHM_VERSE: 42,
|
||||||
|
ChuniConstants.VER_CHUNITHM_X_VERSE: 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
for version, keys in self.game_cfg.crypto.keys.items():
|
for version, keys in self.game_cfg.crypto.keys.items():
|
||||||
@@ -280,8 +283,10 @@ class ChuniServlet(BaseServlet):
|
|||||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||||
elif version >= 225 and version < 230: # LUMINOUS PLUS
|
elif version >= 225 and version < 230: # LUMINOUS PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
|
||||||
elif version >= 230: # VERSE
|
elif version >= 230 and version < 240: # VERSE
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
|
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
|
||||||
|
elif version >= 240: # X-VERSE
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_X_VERSE
|
||||||
elif game_code == "SDGS": # Int
|
elif game_code == "SDGS": # Int
|
||||||
if version < 105: # SUPERSTAR
|
if version < 105: # SUPERSTAR
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
@@ -299,8 +304,10 @@ class ChuniServlet(BaseServlet):
|
|||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||||
elif version >= 130 and version < 135: # LUMINOUS
|
elif version >= 130 and version < 135: # LUMINOUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||||
elif version >= 135: # LUMINOUS PLUS
|
elif version >= 135 and version < 140: # LUMINOUS PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
|
||||||
|
elif version >= 140:
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
|
||||||
elif game_code == "SDHJ": # Chn
|
elif game_code == "SDHJ": # Chn
|
||||||
if version < 110: # NEW
|
if version < 110: # NEW
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class ChuniNew(ChuniBase):
|
|||||||
return "225"
|
return "225"
|
||||||
elif self.version == ChuniConstants.VER_CHUNITHM_VERSE:
|
elif self.version == ChuniConstants.VER_CHUNITHM_VERSE:
|
||||||
return "230"
|
return "230"
|
||||||
|
elif self.version == ChuniConstants.VER_CHUNITHM_X_VERSE:
|
||||||
|
return "240"
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class ChuniReader(BaseReader):
|
|||||||
await self.read_map_icon(f"{dir}/mapIcon", this_opt_id)
|
await self.read_map_icon(f"{dir}/mapIcon", this_opt_id)
|
||||||
await self.read_system_voice(f"{dir}/systemVoice", this_opt_id)
|
await self.read_system_voice(f"{dir}/systemVoice", this_opt_id)
|
||||||
await self.read_unlock_challenge(f"{dir}/unlockChallenge")
|
await self.read_unlock_challenge(f"{dir}/unlockChallenge")
|
||||||
|
await self.read_linked_verse(f"{dir}/linkedVerse")
|
||||||
|
|
||||||
async def read_login_bonus(self, root_dir: str, opt_id: Optional[int] = None) -> None:
|
async def read_login_bonus(self, root_dir: str, opt_id: Optional[int] = None) -> None:
|
||||||
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
||||||
@@ -536,6 +537,38 @@ class ChuniReader(BaseReader):
|
|||||||
self.logger.info(f"Inserted unlock challenge {id}")
|
self.logger.info(f"Inserted unlock challenge {id}")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Failed to unlock challenge {id}")
|
self.logger.warning(f"Failed to unlock challenge {id}")
|
||||||
|
|
||||||
|
async def read_linked_verse(self, lv_dir: str) -> None:
|
||||||
|
for root, dirs, files in walk(lv_dir):
|
||||||
|
for dir in dirs:
|
||||||
|
if path.exists(f"{root}/{dir}/LinkedVerse.xml"):
|
||||||
|
with open(f"{root}/{dir}/LinkedVerse.xml", "r", encoding="utf-8") as fp:
|
||||||
|
strdata = fp.read()
|
||||||
|
|
||||||
|
xml_root = ET.fromstring(strdata)
|
||||||
|
for name in xml_root.findall("name"):
|
||||||
|
id = name.find("id").text
|
||||||
|
name = name.find("str").text
|
||||||
|
|
||||||
|
course_ids = []
|
||||||
|
for course in xml_root.find("musicList/list/LinkedVerseMusicListSubData/linkedVerseMusicData/courseList/list").findall("LinkedVerseCourseListSubData"):
|
||||||
|
course_id = course.find("linkedVerseCourseData/courseName").find("id").text
|
||||||
|
course_ids.append(course_id)
|
||||||
|
|
||||||
|
# Build keyword arguments dynamically for up to 5 course IDs
|
||||||
|
course_kwargs = {
|
||||||
|
f"course_id{i+1}": course_ids[i]
|
||||||
|
for i in range(min(5, len(course_ids)))
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await self.data.static.put_linked_verse(
|
||||||
|
self.version, id, name,
|
||||||
|
**course_kwargs
|
||||||
|
)
|
||||||
|
if result is not None:
|
||||||
|
self.logger.info(f"Inserted Linked VERSE {id}")
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Failed to Linked VERSE {id}")
|
||||||
|
|
||||||
def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None:
|
def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None:
|
||||||
# Convert the image to webp so we can easily display it in the frontend
|
# Convert the image to webp so we can easily display it in the frontend
|
||||||
|
|||||||
@@ -314,6 +314,37 @@ unlock_challenge = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
linked_verse: Table = Table(
|
||||||
|
"chuni_item_linked_verse",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column(
|
||||||
|
"user",
|
||||||
|
Integer,
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("linkedVerseId", Integer, nullable=False),
|
||||||
|
Column("progress", String(255)),
|
||||||
|
Column("statusOpen", Integer),
|
||||||
|
Column("statusUnlock", Integer),
|
||||||
|
Column("isFirstClear", Integer),
|
||||||
|
Column("numClear", Integer),
|
||||||
|
Column("clearCourseId", Integer),
|
||||||
|
Column("clearCourseLevel", Integer),
|
||||||
|
Column("clearScore", Integer),
|
||||||
|
Column("clearDate", String(25)),
|
||||||
|
Column("clearUserId1", Integer),
|
||||||
|
Column("clearUserId2", Integer),
|
||||||
|
Column("clearUserId3", Integer),
|
||||||
|
Column("clearUserName0", String(20)),
|
||||||
|
Column("clearUserName1", String(20)),
|
||||||
|
Column("clearUserName2", String(20)),
|
||||||
|
Column("clearUserName3", String(20)),
|
||||||
|
UniqueConstraint("user", "linkedVerseId", name="chuni_item_linked_verse_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]:
|
||||||
@@ -394,7 +425,6 @@ class ChuniItemData(BaseData):
|
|||||||
async def is_favorite(
|
async def is_favorite(
|
||||||
self, user_id: int, version: int, fav_id: int, fav_kind: int = 1
|
self, user_id: int, version: int, fav_id: int, fav_kind: int = 1
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
sql = favorite.select(
|
sql = favorite.select(
|
||||||
and_(
|
and_(
|
||||||
favorite.c.version == version,
|
favorite.c.version == version,
|
||||||
@@ -849,3 +879,22 @@ class ChuniItemData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def get_linked_verse(self, aime_id: int) -> Optional[List[Row]]:
|
||||||
|
result = await self.execute(
|
||||||
|
linked_verse.select().where(linked_verse.c.user == aime_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def put_linked_verse(self, aime_id: int, linked_verse_data: Dict):
|
||||||
|
linked_verse_data = self.fix_bools(linked_verse_data)
|
||||||
|
sql = insert(linked_verse).values(user=aime_id, **linked_verse_data)
|
||||||
|
conflict = sql.on_duplicate_key_update(**linked_verse_data)
|
||||||
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result.inserted_primary_key["id"]
|
||||||
|
|
||||||
|
self.logger.error("Failed to put Linked Verse data for user %s", aime_id)
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ profile = Table(
|
|||||||
Column("avatarFront", Integer, server_default="0"),
|
Column("avatarFront", Integer, server_default="0"),
|
||||||
Column("avatarSkin", Integer, server_default="0"),
|
Column("avatarSkin", Integer, server_default="0"),
|
||||||
Column("avatarHead", Integer, server_default="0"),
|
Column("avatarHead", Integer, server_default="0"),
|
||||||
|
Column(
|
||||||
|
"stageId", Integer, server_default="99999", nullable=False
|
||||||
|
), # 99999 is the pseudo stage ID for unset stage
|
||||||
UniqueConstraint("user", "version", name="chuni_profile_profile_uk"),
|
UniqueConstraint("user", "version", name="chuni_profile_profile_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -310,6 +310,27 @@ unlock_challenge = Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
linked_verse: Table = Table(
|
||||||
|
"chuni_static_linked_verse",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("version", Integer, nullable=False),
|
||||||
|
Column("linkedVerseId", Integer, nullable=False),
|
||||||
|
Column("name", String(255)),
|
||||||
|
Column("isEnabled", Boolean, server_default="1", nullable=False),
|
||||||
|
Column("startDate", TIMESTAMP, server_default=func.now()),
|
||||||
|
Column("courseId1", Integer),
|
||||||
|
Column("courseId2", Integer),
|
||||||
|
Column("courseId3", Integer),
|
||||||
|
Column("courseId4", Integer),
|
||||||
|
Column("courseId5", Integer),
|
||||||
|
UniqueConstraint(
|
||||||
|
"version", "linkedVerseId", name="chuni_static_linked_verse_pk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniStaticData(BaseData):
|
class ChuniStaticData(BaseData):
|
||||||
async def put_login_bonus(
|
async def put_login_bonus(
|
||||||
self,
|
self,
|
||||||
@@ -1232,3 +1253,57 @@ class ChuniStaticData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def put_linked_verse(
|
||||||
|
self,
|
||||||
|
version: int,
|
||||||
|
linked_verse_id: int,
|
||||||
|
name: str,
|
||||||
|
course_id1: Optional[int] = None,
|
||||||
|
course_id2: Optional[int] = None,
|
||||||
|
course_id3: Optional[int] = None,
|
||||||
|
course_id4: Optional[int] = None,
|
||||||
|
course_id5: Optional[int] = None,
|
||||||
|
) -> Optional[int]:
|
||||||
|
|
||||||
|
sql = insert(linked_verse).values(
|
||||||
|
version=version,
|
||||||
|
linkedVerseId=linked_verse_id,
|
||||||
|
name=name,
|
||||||
|
courseId1=course_id1,
|
||||||
|
courseId2=course_id2,
|
||||||
|
courseId3=course_id3,
|
||||||
|
courseId4=course_id4,
|
||||||
|
courseId5=course_id5,
|
||||||
|
)
|
||||||
|
|
||||||
|
conflict = sql.on_duplicate_key_update(
|
||||||
|
name=name,
|
||||||
|
courseId1=course_id1,
|
||||||
|
courseId2=course_id2,
|
||||||
|
courseId3=course_id3,
|
||||||
|
courseId4=course_id4,
|
||||||
|
courseId5=course_id5,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
async def get_linked_verses(self, version: int) -> Optional[List[Dict]]:
|
||||||
|
sql = linked_verse.select(
|
||||||
|
and_(
|
||||||
|
linked_verse.c.version == version,
|
||||||
|
linked_verse.c.isEnabled == True,
|
||||||
|
)
|
||||||
|
).order_by(linked_verse.c.startDate.asc())
|
||||||
|
|
||||||
|
result = await self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.fetchall()
|
||||||
|
|||||||
317
titles/chuni/xverse.py
Normal file
317
titles/chuni/xverse.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
from .config import ChuniConfig
|
||||||
|
from .const import (
|
||||||
|
ChuniConstants,
|
||||||
|
LinkedVerseUnlockConditionType,
|
||||||
|
MapAreaConditionLogicalOperator,
|
||||||
|
MapAreaConditionType,
|
||||||
|
)
|
||||||
|
from .luminous import MysticAreaConditions
|
||||||
|
from .verse import ChuniVerse
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniXVerse(ChuniVerse):
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg)
|
||||||
|
self.version = ChuniConstants.VER_CHUNITHM_X_VERSE
|
||||||
|
|
||||||
|
async def handle_c_m_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_data = await super().handle_c_m_get_user_preview_api_request(data)
|
||||||
|
|
||||||
|
# Does CARD MAKER 1.35 work this far up?
|
||||||
|
user_data["lastDataVersion"] = "2.40.00"
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
|
||||||
|
events = await self.data.static.get_enabled_events(self.version)
|
||||||
|
|
||||||
|
if events is None:
|
||||||
|
return {"length": 0, "gameMapAreaConditionList": []}
|
||||||
|
|
||||||
|
events_by_id = {event["eventId"]: event for event in events}
|
||||||
|
mystic_conditions = MysticAreaConditions(
|
||||||
|
events_by_id, 3239201, self.date_time_format
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 2 unlocks when VERSE ep. ORIGIN is finished.
|
||||||
|
mystic_conditions.add_condition(17021, 3020803, 3239202)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 3 unlocks when VERSE ep. AIR is finished.
|
||||||
|
mystic_conditions.add_condition(17104, 3020804, 3239203)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 4 unlocks when VERSE ep. STAR is finished.
|
||||||
|
mystic_conditions.add_condition(17208, 3020805, 3239204)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 5 unlocks when VERSE ep. AMAZON is finished.
|
||||||
|
mystic_conditions.add_condition(17304, 3020806, 3239205)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 6 unlocks when VERSE ep. CRYSTAL is finished.
|
||||||
|
mystic_conditions.add_condition(17407, 3020807, 3239206)
|
||||||
|
|
||||||
|
# Mystic Rainbow of X-VERSE Area 7 unlocks when VERSE ep. PARADISE is finished.
|
||||||
|
mystic_conditions.add_condition(17483, 3020808, 3239207)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(mystic_conditions.conditions),
|
||||||
|
"gameMapAreaConditionList": mystic_conditions.conditions,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_game_course_level_api_request(self, data: Dict) -> Dict:
|
||||||
|
uc_likes = [] # includes both UCs and LVs, though the former doesn't show up at all in X-VERSE
|
||||||
|
unlock_challenges, linked_verses = await asyncio.gather(
|
||||||
|
self.data.static.get_unlock_challenges(self.version),
|
||||||
|
self.data.static.get_linked_verses(self.version),
|
||||||
|
)
|
||||||
|
|
||||||
|
if unlock_challenges:
|
||||||
|
uc_likes.extend(unlock_challenges)
|
||||||
|
|
||||||
|
if linked_verses:
|
||||||
|
uc_likes.extend(linked_verses)
|
||||||
|
|
||||||
|
if not uc_likes:
|
||||||
|
return {"length": 0, "gameCourseLevelList": []}
|
||||||
|
|
||||||
|
course_level_list = []
|
||||||
|
current_time = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
for uc_like in uc_likes:
|
||||||
|
course_ids = [
|
||||||
|
uc_like[f"courseId{i}"]
|
||||||
|
for i in range(1, 6)
|
||||||
|
if uc_like[f"courseId{i}"] is not None
|
||||||
|
]
|
||||||
|
event_start_date = uc_like["startDate"].replace(hour=0, minute=0, second=0)
|
||||||
|
|
||||||
|
for i, course_id in enumerate(course_ids):
|
||||||
|
start_date = event_start_date + timedelta(days=7 * i)
|
||||||
|
|
||||||
|
if i == len(course_ids) - 1:
|
||||||
|
end_date = datetime(2099, 12, 31, 23, 59, 59)
|
||||||
|
else:
|
||||||
|
end_date = (
|
||||||
|
event_start_date
|
||||||
|
+ timedelta(days=7 * (i + 1))
|
||||||
|
- timedelta(seconds=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if start_date <= current_time <= end_date:
|
||||||
|
course_level_list.append(
|
||||||
|
{
|
||||||
|
"courseId": course_id,
|
||||||
|
"startDate": start_date.strftime(self.date_time_format),
|
||||||
|
"endDate": end_date.strftime(self.date_time_format),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(course_level_list),
|
||||||
|
"gameCourseLevelList": course_level_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_game_l_v_condition_open_api_request(self, data: Dict) -> Dict:
|
||||||
|
linked_verses = await self.data.static.get_linked_verses(self.version)
|
||||||
|
|
||||||
|
if not linked_verses:
|
||||||
|
return {"length": 0, "gameLinkedVerseConditionOpenList": []}
|
||||||
|
|
||||||
|
linked_verse_by_id = {r["linkedVerseId"]: r for r in linked_verses}
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
for lv_id, map_id in [
|
||||||
|
(10001, 3020803), # ORIGIN
|
||||||
|
(10002, 3020804), # AIR
|
||||||
|
(10003, 3020805), # STAR
|
||||||
|
(10004, 3020806), # AMAZON
|
||||||
|
(10005, 3020807), # CRYSTAL
|
||||||
|
(10006, 3020808), # PARADISE
|
||||||
|
]:
|
||||||
|
if (lv := linked_verse_by_id.get(lv_id)) is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": lv["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||||
|
"conditionId": map_id,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": lv["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 23:59:59",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(conditions),
|
||||||
|
"gameLinkedVerseConditionOpenList": conditions,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_game_l_v_condition_unlock_api_request(
|
||||||
|
self, data: Dict
|
||||||
|
) -> Dict:
|
||||||
|
linked_verses = await self.data.static.get_linked_verses(self.version)
|
||||||
|
|
||||||
|
if not linked_verses:
|
||||||
|
return {
|
||||||
|
"length": 0,
|
||||||
|
"gameLinkedVerseConditionUnlockList": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
linked_verse_by_id = {r["linkedVerseId"]: r for r in linked_verses}
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
# For reference on official Linked VERSE conditions:
|
||||||
|
# https://docs.google.com/spreadsheets/d/1j7kmCR0-R5W3uivwkw-6A_eUCXttnJLnkTO0Qf7dya0/edit?usp=sharing
|
||||||
|
|
||||||
|
# Linked GATE ORIGIN - Play 30 ORIGIN Fables songs
|
||||||
|
if gate_origin := linked_verse_by_id.get(10001):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_origin["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.PLAY_SONGS.value,
|
||||||
|
"conditionList": "59;79;148;71;75;140;163;80;51;64;65;74;95;67;53;100;108;107;105;82;76;141;63;147;69;151;70;101;152;180",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_origin["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked GATE AIR - Obtain class banner
|
||||||
|
if gate_air := linked_verse_by_id.get(10002):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_air["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.COURSE_CLEAR_AND_CLASS_EMBLEM.value,
|
||||||
|
"conditionList": "1_2_3_4_5_6",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_air["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked GATE STAR - Obtain a trophy by leveling a character to level 15
|
||||||
|
if gate_star := linked_verse_by_id.get(10003):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_star["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionList": "9718",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_star["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked GATE AMAZON - Play Killing Rhythm and Climax from the favorites folder
|
||||||
|
if gate_amazon := linked_verse_by_id.get(10004):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_amazon["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.PLAY_SONGS_IN_FAVORITE.value,
|
||||||
|
"conditionList": "712;777",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_amazon["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked GATE CRYSTAL - Clear team course while equipping a character of minimum rank 26
|
||||||
|
if gate_crystal := linked_verse_by_id.get(10005):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_crystal["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.CLEAR_TEAM_COURSE_WITH_CHARACTER_OF_MINIMUM_RANK.value,
|
||||||
|
"conditionList": "26",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_crystal["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked GATE PARADISE - Play one solo song by each of the artists in Inori
|
||||||
|
if gate_paradise := linked_verse_by_id.get(10006):
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"linkedVerseId": gate_paradise["linkedVerseId"],
|
||||||
|
"length": 1,
|
||||||
|
"conditionList": [
|
||||||
|
{
|
||||||
|
"type": LinkedVerseUnlockConditionType.PLAY_SONGS.value,
|
||||||
|
"conditionList": "180_384_2355;407_2353;788_629_600;2704;2050_2354",
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": gate_paradise["startDate"].strftime(
|
||||||
|
self.date_time_format
|
||||||
|
),
|
||||||
|
"endDate": "2099-12-31 00:00:00",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(conditions),
|
||||||
|
"gameLinkedVerseConditionUnlockList": conditions,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_user_l_v_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_id = int(data["userId"])
|
||||||
|
rows = await self.data.item.get_linked_verse(user_id) or []
|
||||||
|
linked_verses = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
data = row._asdict()
|
||||||
|
data.pop("id")
|
||||||
|
data.pop("user")
|
||||||
|
|
||||||
|
linked_verses.append(data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": user_id,
|
||||||
|
"userLinkedVerseList": linked_verses,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user