mirror of
https://gitea.tendokyu.moe/Hay1tsme/artemis.git
synced 2026-02-12 18:57:29 +08:00
Compare commits
8 Commits
5ba0c8b04c
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d75f62bcb4 | ||
|
|
04019da9ac | ||
|
|
d91c21d047 | ||
|
|
b3824f038f | ||
|
|
f2afb3cff5 | ||
|
|
8408d30dc5 | ||
|
|
2cbf34dc28 | ||
|
|
29a52d2712 |
@@ -0,0 +1,31 @@
|
||||
"""chuni_subtrophy_db_fix
|
||||
|
||||
Revision ID: 318d52559e83
|
||||
Revises: 8b57e9646449
|
||||
Create Date: 2026-01-08 19:13:29.803912
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '318d52559e83'
|
||||
down_revision = '8b57e9646449'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('chuni_profile_data', 'trophyIdSub1', existing_type=mysql.INTEGER(), server_default='-1')
|
||||
op.alter_column('chuni_profile_data', 'trophyIdSub2', existing_type=mysql.INTEGER(), server_default='-1')
|
||||
|
||||
# fix any current profiles where the bad defaults were used
|
||||
op.execute("UPDATE chuni_profile_data SET trophyIdSub1=-1 WHERE trophyIdSub1 IS NULL")
|
||||
op.execute("UPDATE chuni_profile_data SET trophyIdSub2=-1 WHERE trophyIdSub2 IS NULL")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# dont bother "unfixing" the table
|
||||
pass
|
||||
98
core/data/alembic/versions/8b57e9646449_chunithm_xverse.py
Normal file
98
core/data/alembic/versions/8b57e9646449_chunithm_xverse.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""CHUNITHM X-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.add_column(
|
||||
"chuni_profile_data",
|
||||
sa.Column("stageId", sa.Integer(), nullable=False, server_default="99999"),
|
||||
)
|
||||
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.create_table(
|
||||
"chuni_static_stage",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("stageId", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(length=255)),
|
||||
sa.Column("imagePath", sa.String(length=255)),
|
||||
sa.Column("isEnabled", sa.Boolean(), server_default="1"),
|
||||
sa.Column("defaultHave", sa.Boolean(), server_default="0"),
|
||||
sa.Column("opt", sa.BIGINT(), sa.ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
sa.UniqueConstraint(
|
||||
"version", "stageId", name="chuni_static_stage_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
# ### 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")
|
||||
op.drop_table("chuni_static_stage")
|
||||
# ### end Alembic commands ###
|
||||
@@ -69,6 +69,7 @@ Games listed below have been tested and confirmed working.
|
||||
| 15 | CHUNITHM LUMINOUS |
|
||||
| 16 | CHUNITHM LUMINOUS PLUS |
|
||||
| 17 | CHUNITHM VERSE |
|
||||
| 18 | CHUNITHM X-VERSE |
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
@@ -11,7 +11,7 @@ mods:
|
||||
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
|
||||
# note: quanity is not refreshed on "continue" after set - only on subsequent login
|
||||
stock_tickets:
|
||||
stock_tickets:
|
||||
stock_count: 99
|
||||
|
||||
# Allow use of all available customization items in frontend web ui
|
||||
@@ -19,12 +19,13 @@ mods:
|
||||
# warning: This can result in pushing a lot of data, especially the userbox items. Recommended for local network use only.
|
||||
forced_item_unlocks:
|
||||
map_icons: False
|
||||
system_voices: False
|
||||
system_voices: False
|
||||
avatar_accessories: False
|
||||
nameplates: False
|
||||
trophies: False
|
||||
character_icons: False
|
||||
|
||||
stages: False
|
||||
|
||||
version:
|
||||
11:
|
||||
rom: 2.00.00
|
||||
@@ -47,6 +48,9 @@ version:
|
||||
17:
|
||||
rom: 2.30.00
|
||||
data: 2.30.00
|
||||
18:
|
||||
rom: 2.40.00
|
||||
data: 2.40.00
|
||||
|
||||
crypto:
|
||||
encrypted_only: False
|
||||
|
||||
@@ -39,6 +39,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ LUMINOUS
|
||||
+ LUMINOUS PLUS
|
||||
+ VERSE
|
||||
+ X-VERSE
|
||||
|
||||
+ crossbeats REV.
|
||||
+ Crossbeats REV.
|
||||
|
||||
@@ -1096,7 +1096,7 @@ class ChuniBase:
|
||||
|
||||
for fav_id in added_ids:
|
||||
await self.data.item.put_favorite_music(user_id, self.version, fav_id)
|
||||
|
||||
|
||||
# added in CHUNITHM VERSE
|
||||
if "userUnlockChallengeList" in upsert:
|
||||
for unlock_challenge in upsert["userUnlockChallengeList"]:
|
||||
@@ -1104,6 +1104,10 @@ class ChuniBase:
|
||||
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"}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class ChuniConstants:
|
||||
VER_CHUNITHM_LUMINOUS = 15
|
||||
VER_CHUNITHM_LUMINOUS_PLUS = 16
|
||||
VER_CHUNITHM_VERSE = 17
|
||||
VER_CHUNITHM_X_VERSE = 18
|
||||
|
||||
VERSION_NAMES = [
|
||||
"CHUNITHM",
|
||||
@@ -48,7 +49,8 @@ class ChuniConstants:
|
||||
"CHUNITHM SUN PLUS",
|
||||
"CHUNITHM LUMINOUS",
|
||||
"CHUNITHM LUMINOUS PLUS",
|
||||
"CHUNITHM VERSE"
|
||||
"CHUNITHM VERSE",
|
||||
"CHUNITHM X-VERSE",
|
||||
]
|
||||
|
||||
SCORE_RANK_INTERVALS_OLD = [
|
||||
@@ -100,6 +102,8 @@ class ChuniConstants:
|
||||
"215": VER_CHUNITHM_SUN_PLUS,
|
||||
"220": VER_CHUNITHM_LUMINOUS,
|
||||
"225": VER_CHUNITHM_LUMINOUS_PLUS,
|
||||
"230": VER_CHUNITHM_VERSE,
|
||||
"240": VER_CHUNITHM_X_VERSE,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -112,31 +116,177 @@ class ChuniConstants:
|
||||
return cls.VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)
|
||||
|
||||
class MapAreaConditionType(IntEnum):
|
||||
"""Condition types for the GetGameMapAreaConditionApi endpoint. Incomplete.
|
||||
"""
|
||||
Condition IDs for the `GetGameMapAreaConditionApi` and `GetGameUCConditionApi` requests.
|
||||
|
||||
For the MAP_CLEARED/MAP_AREA_CLEARED/TROPHY_OBTAINED conditions, the conditionId
|
||||
is the map/map area/trophy.
|
||||
|
||||
For the RANK_*/ALL_JUSTICE conditions, the conditionId is songId * 100 + difficultyId.
|
||||
For example, Halcyon [ULTIMA] would be 173 * 100 + 4 = 17304.
|
||||
- "Item" or "locked item" refers to the map area, unlock challenge or
|
||||
Linked VERSE locked using this system.
|
||||
- "Chart ID" refers to musicID \\* 100 + difficulty, where difficulty is 0 for BASIC
|
||||
up to 6 for WORLD'S END. For example, Halcyon ULTIMA is 17305.
|
||||
"""
|
||||
|
||||
INVALID = 0
|
||||
"""
|
||||
Invalid condition type. Should cause the hidden item to be automatically unlocked,
|
||||
but seemingly only works with map areas.
|
||||
"""
|
||||
|
||||
ALWAYS_UNLOCKED = 0
|
||||
|
||||
MAP_CLEARED = 1
|
||||
"""Finish the map with ID `conditionId`."""
|
||||
|
||||
MAP_AREA_CLEARED = 2
|
||||
|
||||
"""Finish the map area with ID `conditionId`."""
|
||||
|
||||
TROPHY_OBTAINED = 3
|
||||
"""Unlock the trophy with ID `conditionId`."""
|
||||
|
||||
TROPHY_EQUIPPED = 4
|
||||
"""
|
||||
Equip the trophy with ID `conditionId`. The item is locked again when the trophy is
|
||||
unequipped.
|
||||
"""
|
||||
|
||||
NAMEPLATE_OBTAINED = 5
|
||||
"""Unlock the nameplate with ID `conditionId`."""
|
||||
|
||||
NAMEPLATE_EQUIPPED = 6
|
||||
"""
|
||||
Equip the nameplate with ID `conditionId`. The item is locked again when the nameplate
|
||||
is unequipped.
|
||||
"""
|
||||
|
||||
CHARACTER_OBTAINED = 7
|
||||
"""Unlock the character with ID `conditionId`."""
|
||||
|
||||
CHARACTER_EQUIPPED = 8
|
||||
"""
|
||||
Equip the character with ID `conditionId`. The item is locked again when the character
|
||||
is unequipped.
|
||||
"""
|
||||
|
||||
CHARACTER_TRANSFORM_EQUIPPED = 9
|
||||
"""
|
||||
Equip the character, with the character transform ID `conditionId`. The item is locked again
|
||||
if the incorrect character is equipped, or the correct character is equipped with the wrong
|
||||
transform.
|
||||
"""
|
||||
|
||||
MUSIC_OBTAINED = 10
|
||||
"""Unlock the music with ID `conditionId`."""
|
||||
|
||||
AVATAR_ACCESSORY_OBTAINED = 11
|
||||
"""Unlock the avatar accessory with ID `conditionId`."""
|
||||
|
||||
AVATAR_ACCESSORY_EQUIPPED = 12
|
||||
"""
|
||||
Equip the avatar accessory with ID `conditionId`. The item is locked again when the avatar
|
||||
accessory is unequipped.
|
||||
"""
|
||||
|
||||
MAP_ICON_OBTAINED = 13
|
||||
"""Unlock the map icon with ID `conditionId`."""
|
||||
|
||||
MAP_ICON_EQUIPPED = 14
|
||||
"""
|
||||
Equip the map icon with ID `conditionId`. The item is locked again when the map icon is
|
||||
unequipped.
|
||||
"""
|
||||
|
||||
SYSTEM_VOICE_OBTAINED = 15
|
||||
"""Unlock the system voice with ID `conditionId`."""
|
||||
|
||||
SYSTEM_VOICE_EQUIPPED = 16
|
||||
"""
|
||||
Equip the system voice with ID `conditionId`. The item is locked again when the system voice
|
||||
is unequipped.
|
||||
"""
|
||||
|
||||
ALL_JUSTICE_CRITICAL = 17
|
||||
"""Obtain ALL JUSTICE CRITICAL on the chart given by `conditionId`."""
|
||||
|
||||
RANK_SSSP = 18
|
||||
"""Obtain rank SSS+ on the chart given by `conditionId`."""
|
||||
|
||||
RANK_SSS = 19
|
||||
"""Obtain rank SSS on the chart given by `conditionId`."""
|
||||
|
||||
RANK_SSP = 20
|
||||
"""Obtain rank SS+ on the chart given by `conditionId`."""
|
||||
|
||||
RANK_SS = 21
|
||||
"""Obtain rank SS on the chart given by `conditionId`."""
|
||||
|
||||
RANK_SP = 22
|
||||
"""Obtain rank S+ on the chart given by `conditionId`."""
|
||||
|
||||
RANK_S = 23
|
||||
"""Obtain rank S on the chart given by `conditionId`."""
|
||||
|
||||
RANK_AAA = 24
|
||||
"""Obtain rank AAA on the chart given by `conditionId`."""
|
||||
|
||||
RANK_AA = 25
|
||||
"""Obtain rank AA on the chart given by `conditionId`."""
|
||||
|
||||
RANK_A = 26
|
||||
"""Obtain rank A on the chart given by `conditionId`."""
|
||||
|
||||
MINIMUM_BEST_30_AVERAGE = 27
|
||||
"""Obtain a best 30 average of at least `conditionId / 100`."""
|
||||
|
||||
ALL_JUSTICE = 28
|
||||
"""Obtain ALL JUSTICE on the chart given by `conditionId`."""
|
||||
|
||||
FULL_COMBO = 29
|
||||
"""Obtain FULL COMBO on the chart given by `conditionId`."""
|
||||
|
||||
UNLOCK_CHALLENGE_DISCOVERED = 30
|
||||
"""Discover/unlock the unlock challenge with ID `conditionId`."""
|
||||
|
||||
UNLOCK_CHALLENGE_CLEARED = 31
|
||||
"""Clear the unlock challenge with ID `conditionId`."""
|
||||
|
||||
MINIMUM_RATING = 32
|
||||
"""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):
|
||||
AND = 1
|
||||
@@ -179,6 +329,8 @@ class ItemKind(IntEnum):
|
||||
"""This only applies to ULTIMA difficulties that are *not* unlocked by
|
||||
reaching S rank on EXPERT difficulty or above.
|
||||
"""
|
||||
|
||||
STAGE = 13
|
||||
|
||||
|
||||
class FavoriteItemKind(IntEnum):
|
||||
|
||||
@@ -108,6 +108,7 @@ class ChuniFrontend(FE_Base):
|
||||
Route("/avatar", self.render_GET_avatar, methods=['GET']),
|
||||
Route("/update.map-icon", self.update_map_icon, methods=['POST']),
|
||||
Route("/update.system-voice", self.update_system_voice, methods=['POST']),
|
||||
Route("/update.stage", self.update_stage, methods=['POST']),
|
||||
Route("/update.userbox", self.update_userbox, methods=['POST']),
|
||||
Route("/update.avatar", self.update_avatar, methods=['POST']),
|
||||
Route("/update.name", self.update_name, methods=['POST']),
|
||||
@@ -141,6 +142,7 @@ class ChuniFrontend(FE_Base):
|
||||
# version here - it'll just end up being empty sets and the jinja will ignore the variables anyway.
|
||||
map_icons, total_map_icons = await self.get_available_map_icons(version, profile)
|
||||
system_voices, total_system_voices = await self.get_available_system_voices(version, profile)
|
||||
stages, total_stages = await self.get_available_stages(version, profile)
|
||||
|
||||
resp = Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
@@ -155,7 +157,9 @@ class ChuniFrontend(FE_Base):
|
||||
map_icons=map_icons,
|
||||
system_voices=system_voices,
|
||||
total_map_icons=total_map_icons,
|
||||
total_system_voices=total_system_voices
|
||||
total_system_voices=total_system_voices,
|
||||
stages=stages,
|
||||
total_stages=total_stages
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
if usr_sesh.chunithm_version >= 0:
|
||||
@@ -404,6 +408,31 @@ class ChuniFrontend(FE_Base):
|
||||
|
||||
return (items, len(rows))
|
||||
|
||||
async def get_available_stages(self, version: int, profile: Row) -> Tuple[List[Dict], int]:
|
||||
if profile is None:
|
||||
return ([], 0)
|
||||
items = dict()
|
||||
rows = await self.data.static.get_stages(version)
|
||||
if rows is None:
|
||||
return (items, 0) # can only happen with old db
|
||||
|
||||
force_unlocked = self.game_cfg.mods.forced_item_unlocks("stages")
|
||||
|
||||
user_stages = []
|
||||
if not force_unlocked:
|
||||
user_stages = await self.data.item.get_items(profile.user, ItemKind.STAGE.value)
|
||||
user_stages = [icon["itemId"] for icon in user_stages] + [profile.stageId]
|
||||
|
||||
for row in rows:
|
||||
if force_unlocked or row["defaultHave"] or row["stageId"] in user_stages:
|
||||
item = dict()
|
||||
item["id"] = row["stageId"]
|
||||
item["name"] = row["name"]
|
||||
item["imagePath"] = path.splitext(row["imagePath"])[0] + ".webp"
|
||||
items[row["stageId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
|
||||
async def get_available_nameplates(self, version: int, profile: Row) -> Tuple[List[Dict], int]:
|
||||
items = dict()
|
||||
rows = await self.data.static.get_nameplates(version)
|
||||
@@ -650,6 +679,22 @@ class ChuniFrontend(FE_Base):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse("/game/chuni/", 303)
|
||||
|
||||
async def update_stage(self, request: Request) -> bytes:
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
form_data = await request.form()
|
||||
new_system_voice: str = form_data.get("id")
|
||||
|
||||
if not new_system_voice:
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
|
||||
if not await self.data.profile.update_stage(usr_sesh.user_id, usr_sesh.chunithm_version, new_system_voice):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse("/game/chuni/", 303)
|
||||
|
||||
async def update_userbox(self, request: Request) -> bytes:
|
||||
usr_sesh = self.validate_session(request)
|
||||
|
||||
4
titles/chuni/img/stage/.gitignore
vendored
Normal file
4
titles/chuni/img/stage/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
@@ -40,6 +40,7 @@ from .sunplus import ChuniSunPlus
|
||||
from .luminous import ChuniLuminous
|
||||
from .luminousplus import ChuniLuminousPlus
|
||||
from .verse import ChuniVerse
|
||||
from .xverse import ChuniXVerse
|
||||
|
||||
|
||||
class ChuniServlet(BaseServlet):
|
||||
@@ -70,7 +71,8 @@ class ChuniServlet(BaseServlet):
|
||||
ChuniSunPlus,
|
||||
ChuniLuminous,
|
||||
ChuniLuminousPlus,
|
||||
ChuniVerse
|
||||
ChuniVerse,
|
||||
ChuniXVerse,
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("chuni")
|
||||
@@ -119,6 +121,9 @@ class ChuniServlet(BaseServlet):
|
||||
f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8,
|
||||
ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56,
|
||||
ChuniConstants.VER_CHUNITHM_VERSE: 42,
|
||||
f"{ChuniConstants.VER_CHUNITHM_VERSE}_chn": 37,
|
||||
ChuniConstants.VER_CHUNITHM_X_VERSE: 14,
|
||||
f"{ChuniConstants.VER_CHUNITHM_X_VERSE}_int": 96,
|
||||
}
|
||||
|
||||
for version, keys in self.game_cfg.crypto.keys.items():
|
||||
@@ -280,8 +285,10 @@ class ChuniServlet(BaseServlet):
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
elif version >= 225 and version < 230: # 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
|
||||
elif version >= 240: # X-VERSE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_X_VERSE
|
||||
elif game_code == "SDGS": # Int
|
||||
if version < 105: # SUPERSTAR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||
@@ -299,8 +306,12 @@ class ChuniServlet(BaseServlet):
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif version >= 130 and version < 135: # 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
|
||||
elif version >= 140 and version < 150: # VERSE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
|
||||
elif version >= 150: # X-VERSE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_X_VERSE
|
||||
elif game_code == "SDHJ": # Chn
|
||||
if version < 110: # NEW
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
@@ -308,8 +319,10 @@ class ChuniServlet(BaseServlet):
|
||||
version >= 110 and version < 120
|
||||
): # NEW *Cursed but needed due to different encryption key
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 120: # LUMINOUS
|
||||
elif version >= 120 and version < 130: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
elif version >= 130: # VERSE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_VERSE
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from datetime import timedelta
|
||||
from typing import Dict
|
||||
|
||||
from sqlalchemy.engine import Row
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import (
|
||||
@@ -11,6 +13,73 @@ from titles.chuni.const import (
|
||||
from titles.chuni.sunplus import ChuniSunPlus
|
||||
|
||||
|
||||
class MysticAreaConditions:
|
||||
"""The "Mystic Rainbow of <VERSION>" map is a special reward map for obtaining
|
||||
rainbow statues. There's one gold statue area that's unlocked when at least one
|
||||
original map is finished, and additional rainbow statue areas are added as new
|
||||
original maps are added.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, events_by_id: dict[int, Row], map_area_1_id: int, date_time_format: str
|
||||
):
|
||||
self.events_by_id = events_by_id
|
||||
self.date_time_format = date_time_format
|
||||
|
||||
self._map_area_1_conditions = {
|
||||
"mapAreaId": map_area_1_id,
|
||||
"length": 0,
|
||||
"mapAreaConditionList": [],
|
||||
}
|
||||
self._map_area_1_added = False
|
||||
self._conditions = []
|
||||
|
||||
@property
|
||||
def conditions(self):
|
||||
return self._conditions
|
||||
|
||||
def add_condition(
|
||||
self, map_flag_event_id: int, condition_map_id: int, mystic_map_area_id: int
|
||||
):
|
||||
if (event := self.events_by_id.get(map_flag_event_id)) is None:
|
||||
return
|
||||
|
||||
start_date = event["startDate"].strftime(self.date_time_format)
|
||||
|
||||
self._map_area_1_conditions["mapAreaConditionList"].append(
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": condition_map_id,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
}
|
||||
)
|
||||
self._map_area_1_conditions["length"] = len(
|
||||
self._map_area_1_conditions["mapAreaConditionList"]
|
||||
)
|
||||
|
||||
if not self._map_area_1_added:
|
||||
self._conditions.append(self._map_area_1_conditions)
|
||||
self._map_area_1_added = True
|
||||
|
||||
self._conditions.append(
|
||||
{
|
||||
"mapAreaId": mystic_map_area_id,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": condition_map_id,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ChuniLuminous(ChuniSunPlus):
|
||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||
super().__init__(core_cfg, game_cfg)
|
||||
@@ -77,18 +146,18 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
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.
|
||||
# However, we can selectively show/hide events as data is imported into the server.
|
||||
events = await self.data.static.get_enabled_events(self.version)
|
||||
events = await self.data.static.get_enabled_events(self.version) or []
|
||||
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||
conditions = []
|
||||
|
||||
# The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area
|
||||
# (ep. I, ep. II, ep. III) are completed.
|
||||
mystic_area_1_conditions = {
|
||||
"mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1
|
||||
"length": 0,
|
||||
"mapAreaConditionList": [],
|
||||
}
|
||||
mystic_area_1_added = False
|
||||
mystic_conditions = MysticAreaConditions(
|
||||
event_by_id, 3229301, self.date_time_format
|
||||
)
|
||||
mystic_conditions.add_condition(14005, 3020701, 3229302)
|
||||
mystic_conditions.add_condition(14251, 3020702, 3229303)
|
||||
mystic_conditions.add_condition(14481, 3020703, 3229304)
|
||||
|
||||
conditions += mystic_conditions.conditions
|
||||
|
||||
# Secret AREA: MUSIC GAME
|
||||
if 14029 in event_by_id:
|
||||
@@ -229,114 +298,6 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
]
|
||||
)
|
||||
|
||||
# LUMINOUS ep. I
|
||||
if 14005 in event_by_id:
|
||||
start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format)
|
||||
|
||||
if not mystic_area_1_added:
|
||||
conditions.append(mystic_area_1_conditions)
|
||||
mystic_area_1_added = True
|
||||
|
||||
mystic_area_1_conditions["length"] += 1
|
||||
mystic_area_1_conditions["mapAreaConditionList"].append(
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020701,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
}
|
||||
)
|
||||
|
||||
conditions.append(
|
||||
{
|
||||
"mapAreaId": 3229302, # Mystic Rainbow of LUMINOUS Area 2,
|
||||
"length": 1,
|
||||
# Unlocks when LUMINOUS ep. I is completed.
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020701,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# LUMINOUS ep. II
|
||||
if 14251 in event_by_id:
|
||||
start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format)
|
||||
|
||||
if not mystic_area_1_added:
|
||||
conditions.append(mystic_area_1_conditions)
|
||||
mystic_area_1_added = True
|
||||
|
||||
mystic_area_1_conditions["length"] += 1
|
||||
mystic_area_1_conditions["mapAreaConditionList"].append(
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020702,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
}
|
||||
)
|
||||
|
||||
conditions.append(
|
||||
{
|
||||
"mapAreaId": 3229303, # Mystic Rainbow of LUMINOUS Area 3,
|
||||
"length": 1,
|
||||
# Unlocks when LUMINOUS ep. II is completed.
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020702,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# LUMINOUS ep. III
|
||||
if 14481 in event_by_id:
|
||||
start_date = event_by_id[14481]["startDate"].strftime(self.date_time_format)
|
||||
|
||||
if not mystic_area_1_added:
|
||||
conditions.append(mystic_area_1_conditions)
|
||||
mystic_area_1_added = True
|
||||
|
||||
mystic_area_1_conditions["length"] += 1
|
||||
mystic_area_1_conditions["mapAreaConditionList"].append(
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020703,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
}
|
||||
)
|
||||
|
||||
conditions.append(
|
||||
{
|
||||
"mapAreaId": 3229304, # Mystic Rainbow of LUMINOUS Area 4,
|
||||
"length": 1,
|
||||
# Unlocks when LUMINOUS ep. III is completed.
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020703,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# 1UM1N0U5 ep. 111
|
||||
if 14483 in event_by_id:
|
||||
start_date = event_by_id[14483]["startDate"].replace(
|
||||
@@ -381,14 +342,14 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
MapAreaConditionType.RANK_SSP.value,
|
||||
MapAreaConditionType.RANK_SP.value,
|
||||
MapAreaConditionType.RANK_S.value,
|
||||
MapAreaConditionType.ALWAYS_UNLOCKED.value,
|
||||
MapAreaConditionType.INVALID.value,
|
||||
]
|
||||
):
|
||||
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
||||
self.date_time_format
|
||||
)
|
||||
|
||||
if typ != MapAreaConditionType.ALWAYS_UNLOCKED.value:
|
||||
if typ != MapAreaConditionType.INVALID.value:
|
||||
end = (
|
||||
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
||||
).strftime(self.date_time_format)
|
||||
@@ -407,7 +368,7 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
)
|
||||
else:
|
||||
end = "2099-12-31 00:00:00"
|
||||
|
||||
|
||||
title_conditions.append(
|
||||
{
|
||||
"type": typ,
|
||||
@@ -431,7 +392,7 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
# Ultimate Force
|
||||
# For the first 14 days, the condition is to obtain all 9 "Key of ..." titles
|
||||
# Afterwards, the condition is the 6 "Key of ..." titles that you can obtain
|
||||
# by playing the 6 areas, as well as obtaining specific ranks on
|
||||
# by playing the 6 areas, as well as obtaining specific ranks on
|
||||
# [CRYSTAL_ACCESS] / Strange Love / βlαnoir
|
||||
ultimate_force_conditions = []
|
||||
|
||||
@@ -473,7 +434,7 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
||||
self.date_time_format
|
||||
)
|
||||
|
||||
|
||||
end = (
|
||||
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
||||
).strftime(self.date_time_format)
|
||||
@@ -487,7 +448,7 @@ class ChuniLuminous(ChuniSunPlus):
|
||||
"startDate": start,
|
||||
"endDate": end,
|
||||
}
|
||||
for condition_id in {109403, 212103, 244203}
|
||||
for condition_id in {109403, 212103, 244203}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Dict
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
|
||||
from titles.chuni.luminous import ChuniLuminous
|
||||
from titles.chuni.luminous import ChuniLuminous, MysticAreaConditions
|
||||
|
||||
|
||||
class ChuniLuminousPlus(ChuniLuminous):
|
||||
@@ -66,6 +66,158 @@ class ChuniLuminousPlus(ChuniLuminous):
|
||||
events = await self.data.static.get_enabled_events(self.version)
|
||||
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||
conditions = []
|
||||
|
||||
mystic_conditions = MysticAreaConditions(
|
||||
event_by_id,
|
||||
3229601,
|
||||
self.date_time_format,
|
||||
)
|
||||
|
||||
# Mystic Rainbow of LUMINOUS PLUS - LUMINOUS ep. IV
|
||||
mystic_conditions.add_condition(15005, 3020704, 3229602)
|
||||
|
||||
# Mystic Rainbow of LUMINOUS PLUS - LUMINOUS ep. V
|
||||
mystic_conditions.add_condition(15306, 3020705, 3229603)
|
||||
|
||||
# Mystic Rainbow of LUMINOUS PLUS - LUMINOUS ep. VI
|
||||
mystic_conditions.add_condition(15451, 3020706, 3229604)
|
||||
|
||||
# Mystic Rainbow of LUMINOUS PLUS - LUMINOUS ep. VII
|
||||
mystic_conditions.add_condition(15506, 3020707, 3229605)
|
||||
|
||||
conditions += mystic_conditions.conditions
|
||||
|
||||
# 1UM1N0U5 ep. 111 continues. The map is automatically unlocked after finishing
|
||||
# LUMINOUS ep. III in LUMINOUS PLUS.
|
||||
if ep_111 := event_by_id.get(15009):
|
||||
start_date = ep_111["startDate"].strftime(self.date_time_format)
|
||||
|
||||
conditions.append({
|
||||
"mapAreaId": 3229207,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020703,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
# ■・■■■■■■・■
|
||||
# Finish LUMINOUS ep. IV and obtain the title 「ここは…何処なんだ…?」.
|
||||
if re_fiction_o := event_by_id.get(15032):
|
||||
start_date = re_fiction_o["startDate"].strftime(self.date_time_format)
|
||||
|
||||
conditions.append({
|
||||
"mapAreaId": 3229501,
|
||||
"length": 2,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 3020704,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
},
|
||||
{
|
||||
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||
"conditionId": 7105,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# The Conductor's Path
|
||||
# ALL JUSTICE CRITICAL 其のエメラルドを見よ MASTER.
|
||||
if the_conductors_path := event_by_id.get(15033):
|
||||
start_date = the_conductors_path["startDate"].strftime(self.date_time_format)
|
||||
|
||||
conditions.append({
|
||||
"mapAreaId": 3229701,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.ALL_JUSTICE_CRITICAL.value,
|
||||
"conditionId": 260003,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Cave of RVESE
|
||||
if episode__x__ := event_by_id.get(15254):
|
||||
start_date = episode__x__["startDate"].strftime(self.date_time_format)
|
||||
|
||||
conditions.extend([
|
||||
# Episode. _ _ X _ _ map area 1
|
||||
# Finish the HARDCORE TANO*C collaboration map.
|
||||
{
|
||||
"mapAreaId": 2208801,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||
"conditionId": 2006533,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
# Episode. _ _ X _ _ map area 2
|
||||
# Equip the title 「第壱の石版【V】」 to access the map area.
|
||||
{
|
||||
"mapAreaId": 2208802,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.TROPHY_EQUIPPED.value,
|
||||
"conditionId": 7107,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
# Episode. _ _ X _ _ map area 3
|
||||
# Equip the title 「第弐の石版【Λ】」 to access the map area.
|
||||
{
|
||||
"mapAreaId": 2208803,
|
||||
"length": 1,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.TROPHY_EQUIPPED.value,
|
||||
"conditionId": 7104,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
# Episode. _ _ X _ _ map area 4
|
||||
# Complete the 3 other map areas.
|
||||
{
|
||||
"mapAreaId": 2208804,
|
||||
"length": 3,
|
||||
"mapAreaConditionList": [
|
||||
{
|
||||
"type": MapAreaConditionType.MAP_AREA_CLEARED.value,
|
||||
"conditionId": area_id,
|
||||
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||
"startDate": start_date,
|
||||
"endDate": "2099-12-31 00:00:00",
|
||||
}
|
||||
for area_id in range(2208801, 2208804)
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
# LUMINOUS ep. Ascension
|
||||
if ep_ascension := event_by_id.get(15512):
|
||||
|
||||
@@ -40,6 +40,8 @@ class ChuniNew(ChuniBase):
|
||||
return "225"
|
||||
elif self.version == ChuniConstants.VER_CHUNITHM_VERSE:
|
||||
return "230"
|
||||
elif self.version == ChuniConstants.VER_CHUNITHM_X_VERSE:
|
||||
return "240"
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
# use UTC time and convert it to JST time by adding +9
|
||||
|
||||
@@ -68,6 +68,9 @@ class ChuniReader(BaseReader):
|
||||
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_unlock_challenge(f"{dir}/unlockChallenge")
|
||||
await self.read_linked_verse(f"{dir}/linkedVerse")
|
||||
if self.version >= ChuniConstants.VER_CHUNITHM_X_VERSE:
|
||||
await self.read_stage(f"{dir}/stage", this_opt_id)
|
||||
|
||||
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"):
|
||||
@@ -536,6 +539,65 @@ class ChuniReader(BaseReader):
|
||||
self.logger.info(f"Inserted unlock challenge {id}")
|
||||
else:
|
||||
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}")
|
||||
|
||||
async def read_stage(self, stage_dir: str, opt_id: Optional[int] = None) -> None:
|
||||
for root, dirs, files in walk(stage_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/Stage.xml"):
|
||||
with open(f"{root}/{dir}/Stage.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
|
||||
for image in xml_root.findall("image"):
|
||||
image_path = image.find("path").text
|
||||
self.copy_image(image_path, f"{root}/{dir}", "titles/chuni/img/stage/")
|
||||
default_have = xml_root.find("defaultHave").text == 'true'
|
||||
disable_flag = xml_root.find("disableFlag") # may not exist in older data
|
||||
is_enabled = True if (disable_flag is None or disable_flag.text == "false") else False
|
||||
|
||||
result = await self.data.static.put_stage(
|
||||
self.version, id, name, image_path, is_enabled, default_have, opt_id
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
self.logger.info(f"Inserted stage {id}")
|
||||
else:
|
||||
self.logger.warning(f"Failed to insert stage {id}")
|
||||
|
||||
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
|
||||
|
||||
@@ -314,6 +314,37 @@ unlock_challenge = Table(
|
||||
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):
|
||||
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
||||
@@ -394,7 +425,6 @@ class ChuniItemData(BaseData):
|
||||
async def is_favorite(
|
||||
self, user_id: int, version: int, fav_id: int, fav_kind: int = 1
|
||||
) -> bool:
|
||||
|
||||
sql = favorite.select(
|
||||
and_(
|
||||
favorite.c.version == version,
|
||||
@@ -849,3 +879,22 @@ class ChuniItemData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
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)
|
||||
|
||||
@@ -26,8 +26,8 @@ profile = Table(
|
||||
Column("frameId", Integer),
|
||||
Column("isMaimai", Boolean),
|
||||
Column("trophyId", Integer),
|
||||
Column("trophyIdSub1", Integer),
|
||||
Column("trophyIdSub2", Integer),
|
||||
Column("trophyIdSub1", Integer, server_default="-1"),
|
||||
Column("trophyIdSub2", Integer, server_default="-1"),
|
||||
Column("userName", String(25)),
|
||||
Column("isWebJoin", Boolean),
|
||||
Column("playCount", Integer),
|
||||
@@ -132,6 +132,9 @@ profile = Table(
|
||||
Column("avatarFront", Integer, server_default="0"),
|
||||
Column("avatarSkin", 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"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -474,6 +477,17 @@ class ChuniProfileData(BaseData):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def update_stage(self, user_id: int, version: int, new_stage: int) -> bool:
|
||||
sql = profile.update((profile.c.user == user_id) & (profile.c.version == version)).values(
|
||||
stageId=new_stage
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to set user {user_id} stage")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def update_userbox(self, user_id: int, version: int, new_nameplate: int, new_trophy: int, new_trophy_sub_1: int, new_trophy_sub_2: int, new_character: int) -> bool:
|
||||
sql = profile.update((profile.c.user == user_id) & (profile.c.version == version)).values(
|
||||
nameplateId=new_nameplate,
|
||||
|
||||
@@ -40,7 +40,7 @@ events = Table(
|
||||
Column("name", String(255)),
|
||||
Column("startDate", TIMESTAMP, server_default=func.now()),
|
||||
Column("enabled", Boolean, server_default="1"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -58,7 +58,7 @@ music = Table(
|
||||
Column("genre", String(255)),
|
||||
Column("jacketPath", String(255)),
|
||||
Column("worldsEndTag", String(7)),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -74,7 +74,7 @@ charge = Table(
|
||||
Column("consumeType", Integer),
|
||||
Column("sellingAppeal", Boolean),
|
||||
Column("enabled", Boolean, server_default="1"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -92,7 +92,7 @@ avatar = Table(
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("sortName", String(255)),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -108,7 +108,7 @@ nameplate = Table(
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("sortName", String(255)),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "nameplateId", name="chuni_static_nameplate_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -128,7 +128,7 @@ character = Table(
|
||||
Column("imagePath3", String(255)),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "characterId", name="chuni_static_character_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -143,7 +143,7 @@ trophy = Table(
|
||||
Column("rareType", Integer),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "trophyId", name="chuni_static_trophy_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -159,7 +159,7 @@ map_icon = Table(
|
||||
Column("iconPath", String(255)),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "mapIconId", name="chuni_static_mapicon_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -175,7 +175,7 @@ system_voice = Table(
|
||||
Column("imagePath", String(255)),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "voiceId", name="chuni_static_systemvoice_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -197,7 +197,7 @@ gachas = Table(
|
||||
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT,ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -218,7 +218,7 @@ cards = Table(
|
||||
Column("combo", Integer, nullable=False),
|
||||
Column("chain", Integer, nullable=False),
|
||||
Column("skillName", String(255), nullable=False),
|
||||
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
Column("opt", BIGINT, ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -304,7 +304,45 @@ unlock_challenge = Table(
|
||||
Column("courseId4", Integer),
|
||||
Column("courseId5", Integer),
|
||||
UniqueConstraint(
|
||||
"version", "unlockChallengeId", name="chuni_static_unlock_challenge_pk"
|
||||
"version", "unlockChallengeId", name="chuni_static_unlock_challenge_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
linked_verse = 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_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
stage = Table(
|
||||
"chuni_static_stage",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("stageId", Integer, nullable=False),
|
||||
Column("name", String(255)),
|
||||
Column("imagePath", String(255)),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint(
|
||||
"version", "stageId", name="chuni_static_stage_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@@ -1232,3 +1270,108 @@ class ChuniStaticData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
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()
|
||||
|
||||
async def put_stage(
|
||||
self,
|
||||
version: int,
|
||||
stage_id: int,
|
||||
name: str,
|
||||
image_path: str,
|
||||
is_enabled: int,
|
||||
default_have: int,
|
||||
opt_id: int = None
|
||||
) -> Optional[int]:
|
||||
|
||||
sql = insert(stage).values(
|
||||
version=version,
|
||||
stageId=stage_id,
|
||||
name=name,
|
||||
imagePath=image_path,
|
||||
isEnabled=is_enabled,
|
||||
defaultHave=default_have,
|
||||
opt=coalesce(stage.c.opt, opt_id)
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
name=name,
|
||||
imagePath=image_path,
|
||||
isEnabled=is_enabled,
|
||||
defaultHave=default_have,
|
||||
opt=coalesce(stage.c.opt, opt_id)
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.lastrowid
|
||||
|
||||
async def get_stages(self, version: int) -> Optional[List[Dict]]:
|
||||
sql = stage.select(
|
||||
and_(
|
||||
stage.c.version == version,
|
||||
stage.c.isEnabled == True,
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.fetchall()
|
||||
|
||||
@@ -79,6 +79,12 @@
|
||||
<td><div id="system-voice-name">{{ system_voices[profile.voiceId]["name"] if system_voices|length > 0 else "Server DB needs upgraded or is not populated with necessary data" }}</div></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if cur_version >= 18 %} <!-- STAGE introduced in X-VERSE -->
|
||||
<tr>
|
||||
<td>Stage:</td>
|
||||
<td><div id="stage-name">{{ stages[profile.stageId]["name"] if stages|length > 0 else "Server DB needs upgraded or is not populated with necessary data" }}</div></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,6 +117,21 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if cur_version >= 18 %} <!-- STAGE introduced in X-VERSE -->
|
||||
<!-- STAGE SELECTION -->
|
||||
<div class="col-lg-8 m-auto mt-3 scrolling-lists">
|
||||
<div class="card bg-card rounded">
|
||||
<button class="collapsible">Stage: {{ stages|length }}/{{ total_stages }}</button>
|
||||
<div id="scrollable-stage" class="collapsible-content">
|
||||
{% for item in stages.values() %}
|
||||
<img id="stage-{{ item["id"] }}" style="padding: 8px 8px;" onclick="saveItem('stage', '{{ item["id"] }}', '{{ item["name"] }}')" src="img/stage/{{ item["imagePath"] }}" alt="{{ item["name"] }}">
|
||||
<span id="stage-br-{{ loop.index }}"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-lg-8 m-auto mt-3">
|
||||
<div class="card bg-card rounded">
|
||||
<table class="table-large table-rowdistinct">
|
||||
@@ -201,6 +222,10 @@ items = {
|
||||
"map-icon": ["{{ map_icons|length }}", "{{ profile.mapIconId }}"],
|
||||
"system-voice":["{{ system_voices|length }}", "{{ profile.voiceId }}"]
|
||||
};
|
||||
// STAGE introduced in X-VERSE
|
||||
if ({{cur_version}} >= 18) {
|
||||
items.stage = ["{{ stages|length }}", "{{ profile.stageId }}"]
|
||||
}
|
||||
types = Object.keys(items);
|
||||
|
||||
function changeItem(type, id, name) {
|
||||
|
||||
@@ -8,6 +8,7 @@ from titles.chuni.const import (
|
||||
MapAreaConditionLogicalOperator,
|
||||
MapAreaConditionType,
|
||||
)
|
||||
from titles.chuni.luminous import MysticAreaConditions
|
||||
from titles.chuni.luminousplus import ChuniLuminousPlus
|
||||
|
||||
|
||||
@@ -22,6 +23,38 @@ class ChuniVerse(ChuniLuminousPlus):
|
||||
# Does CARD MAKER 1.35 work this far up?
|
||||
user_data["lastDataVersion"] = "2.30.00"
|
||||
return user_data
|
||||
|
||||
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.
|
||||
# However, we can selectively show/hide events as data is imported into the server.
|
||||
events = await self.data.static.get_enabled_events(self.version)
|
||||
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||
conditions = []
|
||||
|
||||
mystic_conditions = MysticAreaConditions(
|
||||
event_by_id,
|
||||
3230401,
|
||||
self.date_time_format,
|
||||
)
|
||||
|
||||
# Mystic Rainbow of VERSE - VERSE ep. I
|
||||
mystic_conditions.add_condition(16006, 3020798, 3230402)
|
||||
|
||||
# Mystic Rainbow of VERSE - VERSE ep. II
|
||||
mystic_conditions.add_condition(16204, 3020799, 3230403)
|
||||
|
||||
# Mystic Rainbow of VERSE - VERSE ep. III
|
||||
mystic_conditions.add_condition(16455, 3020800, 3230404)
|
||||
|
||||
# Mystic Rainbow of VERSE - VERSE ep. IV
|
||||
mystic_conditions.add_condition(16607, 3020802, 3230405)
|
||||
|
||||
conditions += mystic_conditions.conditions
|
||||
|
||||
return {
|
||||
"length": len(conditions),
|
||||
"gameMapAreaConditionList": conditions,
|
||||
}
|
||||
|
||||
async def handle_get_game_course_level_api_request(self, data: Dict) -> Dict:
|
||||
unlock_challenges = await self.data.static.get_unlock_challenges(self.version)
|
||||
@@ -81,9 +114,10 @@ class ChuniVerse(ChuniLuminousPlus):
|
||||
|
||||
unlock_condition = conditions.get(
|
||||
unlock_challenge_id,
|
||||
# default is to unlock for players above 5.00 rating
|
||||
{
|
||||
"type": MapAreaConditionType.TROPHY_OBTAINED.value, # always unlocked
|
||||
"conditionId": 0,
|
||||
"type": MapAreaConditionType.MINIMUM_RATING.value,
|
||||
"conditionId": 500,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -173,7 +207,7 @@ class ChuniVerse(ChuniLuminousPlus):
|
||||
|
||||
user_rec_music_list = [
|
||||
{
|
||||
"musicId": 1, # no idea
|
||||
"musicId": 1, # a song the player recently played
|
||||
# recMusicList is a semi colon-separated list of music IDs and their order comma separated
|
||||
# for some reason, not all music ids are shown in game?!
|
||||
"recMusicList": ";".join(
|
||||
@@ -193,7 +227,8 @@ class ChuniVerse(ChuniLuminousPlus):
|
||||
class UserRecRating:
|
||||
ratingMin: int
|
||||
ratingMax: int
|
||||
# same as recMusicList in get_user_rec_music_api_request
|
||||
# semicolon-delimited list of (musicId, level, sortingKey, score), in the
|
||||
# same format as GetUserRecMusicApi
|
||||
recMusicList: str
|
||||
|
||||
length: int
|
||||
|
||||
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