mirror of
https://gitea.tendokyu.moe/Hay1tsme/artemis.git
synced 2026-02-13 03:07: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 |
|
| 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,13 @@ 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
|
||||||
|
stages: False
|
||||||
|
|
||||||
version:
|
version:
|
||||||
11:
|
11:
|
||||||
rom: 2.00.00
|
rom: 2.00.00
|
||||||
@@ -47,6 +48,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
|
||||||
@@ -112,31 +116,177 @@ class ChuniConstants:
|
|||||||
return cls.VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)
|
return cls.VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)
|
||||||
|
|
||||||
class MapAreaConditionType(IntEnum):
|
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
|
- "Item" or "locked item" refers to the map area, unlock challenge or
|
||||||
is the map/map area/trophy.
|
Linked VERSE locked using this system.
|
||||||
|
- "Chart ID" refers to musicID \\* 100 + difficulty, where difficulty is 0 for BASIC
|
||||||
For the RANK_*/ALL_JUSTICE conditions, the conditionId is songId * 100 + difficultyId.
|
up to 6 for WORLD'S END. For example, Halcyon ULTIMA is 17305.
|
||||||
For example, Halcyon [ULTIMA] would be 173 * 100 + 4 = 17304.
|
"""
|
||||||
|
|
||||||
|
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
|
MAP_CLEARED = 1
|
||||||
|
"""Finish the map with ID `conditionId`."""
|
||||||
|
|
||||||
MAP_AREA_CLEARED = 2
|
MAP_AREA_CLEARED = 2
|
||||||
|
"""Finish the map area with ID `conditionId`."""
|
||||||
|
|
||||||
TROPHY_OBTAINED = 3
|
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
|
RANK_SSSP = 18
|
||||||
|
"""Obtain rank SSS+ on the chart given by `conditionId`."""
|
||||||
|
|
||||||
RANK_SSS = 19
|
RANK_SSS = 19
|
||||||
|
"""Obtain rank SSS on the chart given by `conditionId`."""
|
||||||
|
|
||||||
RANK_SSP = 20
|
RANK_SSP = 20
|
||||||
|
"""Obtain rank SS+ on the chart given by `conditionId`."""
|
||||||
|
|
||||||
RANK_SS = 21
|
RANK_SS = 21
|
||||||
|
"""Obtain rank SS on the chart given by `conditionId`."""
|
||||||
|
|
||||||
RANK_SP = 22
|
RANK_SP = 22
|
||||||
|
"""Obtain rank S+ on the chart given by `conditionId`."""
|
||||||
|
|
||||||
RANK_S = 23
|
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
|
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):
|
class MapAreaConditionLogicalOperator(Enum):
|
||||||
AND = 1
|
AND = 1
|
||||||
@@ -179,6 +329,8 @@ class ItemKind(IntEnum):
|
|||||||
"""This only applies to ULTIMA difficulties that are *not* unlocked by
|
"""This only applies to ULTIMA difficulties that are *not* unlocked by
|
||||||
reaching S rank on EXPERT difficulty or above.
|
reaching S rank on EXPERT difficulty or above.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
STAGE = 13
|
||||||
|
|
||||||
|
|
||||||
class FavoriteItemKind(IntEnum):
|
class FavoriteItemKind(IntEnum):
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class ChuniFrontend(FE_Base):
|
|||||||
Route("/avatar", self.render_GET_avatar, methods=['GET']),
|
Route("/avatar", self.render_GET_avatar, methods=['GET']),
|
||||||
Route("/update.map-icon", self.update_map_icon, methods=['POST']),
|
Route("/update.map-icon", self.update_map_icon, methods=['POST']),
|
||||||
Route("/update.system-voice", self.update_system_voice, 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.userbox", self.update_userbox, methods=['POST']),
|
||||||
Route("/update.avatar", self.update_avatar, methods=['POST']),
|
Route("/update.avatar", self.update_avatar, methods=['POST']),
|
||||||
Route("/update.name", self.update_name, 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.
|
# 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)
|
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)
|
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(
|
resp = Response(template.render(
|
||||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
@@ -155,7 +157,9 @@ class ChuniFrontend(FE_Base):
|
|||||||
map_icons=map_icons,
|
map_icons=map_icons,
|
||||||
system_voices=system_voices,
|
system_voices=system_voices,
|
||||||
total_map_icons=total_map_icons,
|
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")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
if usr_sesh.chunithm_version >= 0:
|
if usr_sesh.chunithm_version >= 0:
|
||||||
@@ -404,6 +408,31 @@ class ChuniFrontend(FE_Base):
|
|||||||
|
|
||||||
return (items, len(rows))
|
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]:
|
async def get_available_nameplates(self, version: int, profile: Row) -> Tuple[List[Dict], int]:
|
||||||
items = dict()
|
items = dict()
|
||||||
rows = await self.data.static.get_nameplates(version)
|
rows = await self.data.static.get_nameplates(version)
|
||||||
@@ -650,6 +679,22 @@ class ChuniFrontend(FE_Base):
|
|||||||
return RedirectResponse("/gate/?e=999", 303)
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
return RedirectResponse("/game/chuni/", 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:
|
async def update_userbox(self, request: Request) -> bytes:
|
||||||
usr_sesh = self.validate_session(request)
|
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 .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,9 @@ 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,
|
||||||
|
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():
|
for version, keys in self.game_cfg.crypto.keys.items():
|
||||||
@@ -280,8 +285,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 +306,12 @@ 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 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
|
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
|
||||||
@@ -308,8 +319,10 @@ class ChuniServlet(BaseServlet):
|
|||||||
version >= 110 and version < 120
|
version >= 110 and version < 120
|
||||||
): # NEW *Cursed but needed due to different encryption key
|
): # NEW *Cursed but needed due to different encryption key
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
elif version >= 120: # LUMINOUS
|
elif version >= 120 and version < 130: # LUMINOUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_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 all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
# If we get a 32 character long hex string, it's a hash and we're
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from sqlalchemy.engine import Row
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
from titles.chuni.const import (
|
from titles.chuni.const import (
|
||||||
@@ -11,6 +13,73 @@ from titles.chuni.const import (
|
|||||||
from titles.chuni.sunplus import ChuniSunPlus
|
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):
|
class ChuniLuminous(ChuniSunPlus):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
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:
|
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.
|
# 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.
|
# 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}
|
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area
|
mystic_conditions = MysticAreaConditions(
|
||||||
# (ep. I, ep. II, ep. III) are completed.
|
event_by_id, 3229301, self.date_time_format
|
||||||
mystic_area_1_conditions = {
|
)
|
||||||
"mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1
|
mystic_conditions.add_condition(14005, 3020701, 3229302)
|
||||||
"length": 0,
|
mystic_conditions.add_condition(14251, 3020702, 3229303)
|
||||||
"mapAreaConditionList": [],
|
mystic_conditions.add_condition(14481, 3020703, 3229304)
|
||||||
}
|
|
||||||
mystic_area_1_added = False
|
conditions += mystic_conditions.conditions
|
||||||
|
|
||||||
# Secret AREA: MUSIC GAME
|
# Secret AREA: MUSIC GAME
|
||||||
if 14029 in event_by_id:
|
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
|
# 1UM1N0U5 ep. 111
|
||||||
if 14483 in event_by_id:
|
if 14483 in event_by_id:
|
||||||
start_date = event_by_id[14483]["startDate"].replace(
|
start_date = event_by_id[14483]["startDate"].replace(
|
||||||
@@ -381,14 +342,14 @@ class ChuniLuminous(ChuniSunPlus):
|
|||||||
MapAreaConditionType.RANK_SSP.value,
|
MapAreaConditionType.RANK_SSP.value,
|
||||||
MapAreaConditionType.RANK_SP.value,
|
MapAreaConditionType.RANK_SP.value,
|
||||||
MapAreaConditionType.RANK_S.value,
|
MapAreaConditionType.RANK_S.value,
|
||||||
MapAreaConditionType.ALWAYS_UNLOCKED.value,
|
MapAreaConditionType.INVALID.value,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
||||||
self.date_time_format
|
self.date_time_format
|
||||||
)
|
)
|
||||||
|
|
||||||
if typ != MapAreaConditionType.ALWAYS_UNLOCKED.value:
|
if typ != MapAreaConditionType.INVALID.value:
|
||||||
end = (
|
end = (
|
||||||
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
||||||
).strftime(self.date_time_format)
|
).strftime(self.date_time_format)
|
||||||
@@ -407,7 +368,7 @@ class ChuniLuminous(ChuniSunPlus):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
end = "2099-12-31 00:00:00"
|
end = "2099-12-31 00:00:00"
|
||||||
|
|
||||||
title_conditions.append(
|
title_conditions.append(
|
||||||
{
|
{
|
||||||
"type": typ,
|
"type": typ,
|
||||||
@@ -431,7 +392,7 @@ class ChuniLuminous(ChuniSunPlus):
|
|||||||
# Ultimate Force
|
# Ultimate Force
|
||||||
# For the first 14 days, the condition is to obtain all 9 "Key of ..." titles
|
# 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
|
# 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
|
# [CRYSTAL_ACCESS] / Strange Love / βlαnoir
|
||||||
ultimate_force_conditions = []
|
ultimate_force_conditions = []
|
||||||
|
|
||||||
@@ -473,7 +434,7 @@ class ChuniLuminous(ChuniSunPlus):
|
|||||||
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
start = (start_date + timedelta(days=14 * (i + 1))).strftime(
|
||||||
self.date_time_format
|
self.date_time_format
|
||||||
)
|
)
|
||||||
|
|
||||||
end = (
|
end = (
|
||||||
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
start_date + timedelta(days=14 * (i + 2)) - timedelta(seconds=1)
|
||||||
).strftime(self.date_time_format)
|
).strftime(self.date_time_format)
|
||||||
@@ -487,7 +448,7 @@ class ChuniLuminous(ChuniSunPlus):
|
|||||||
"startDate": start,
|
"startDate": start,
|
||||||
"endDate": end,
|
"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 core.config import CoreConfig
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
|
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
|
||||||
from titles.chuni.luminous import ChuniLuminous
|
from titles.chuni.luminous import ChuniLuminous, MysticAreaConditions
|
||||||
|
|
||||||
|
|
||||||
class ChuniLuminousPlus(ChuniLuminous):
|
class ChuniLuminousPlus(ChuniLuminous):
|
||||||
@@ -66,6 +66,158 @@ class ChuniLuminousPlus(ChuniLuminous):
|
|||||||
events = await self.data.static.get_enabled_events(self.version)
|
events = await self.data.static.get_enabled_events(self.version)
|
||||||
event_by_id = {evt["eventId"]: evt for evt in events}
|
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||||
conditions = []
|
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
|
# LUMINOUS ep. Ascension
|
||||||
if ep_ascension := event_by_id.get(15512):
|
if ep_ascension := event_by_id.get(15512):
|
||||||
|
|||||||
@@ -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,9 @@ 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")
|
||||||
|
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:
|
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 +539,65 @@ 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}")
|
||||||
|
|
||||||
|
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:
|
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)
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ profile = Table(
|
|||||||
Column("frameId", Integer),
|
Column("frameId", Integer),
|
||||||
Column("isMaimai", Boolean),
|
Column("isMaimai", Boolean),
|
||||||
Column("trophyId", Integer),
|
Column("trophyId", Integer),
|
||||||
Column("trophyIdSub1", Integer),
|
Column("trophyIdSub1", Integer, server_default="-1"),
|
||||||
Column("trophyIdSub2", Integer),
|
Column("trophyIdSub2", Integer, server_default="-1"),
|
||||||
Column("userName", String(25)),
|
Column("userName", String(25)),
|
||||||
Column("isWebJoin", Boolean),
|
Column("isWebJoin", Boolean),
|
||||||
Column("playCount", Integer),
|
Column("playCount", Integer),
|
||||||
@@ -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",
|
||||||
)
|
)
|
||||||
@@ -474,6 +477,17 @@ class ChuniProfileData(BaseData):
|
|||||||
return False
|
return False
|
||||||
return True
|
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:
|
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(
|
sql = profile.update((profile.c.user == user_id) & (profile.c.version == version)).values(
|
||||||
nameplateId=new_nameplate,
|
nameplateId=new_nameplate,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ events = Table(
|
|||||||
Column("name", String(255)),
|
Column("name", String(255)),
|
||||||
Column("startDate", TIMESTAMP, server_default=func.now()),
|
Column("startDate", TIMESTAMP, server_default=func.now()),
|
||||||
Column("enabled", Boolean, server_default="1"),
|
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"),
|
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ music = Table(
|
|||||||
Column("genre", String(255)),
|
Column("genre", String(255)),
|
||||||
Column("jacketPath", String(255)),
|
Column("jacketPath", String(255)),
|
||||||
Column("worldsEndTag", String(7)),
|
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"),
|
UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -74,7 +74,7 @@ charge = Table(
|
|||||||
Column("consumeType", Integer),
|
Column("consumeType", Integer),
|
||||||
Column("sellingAppeal", Boolean),
|
Column("sellingAppeal", Boolean),
|
||||||
Column("enabled", Boolean, server_default="1"),
|
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"),
|
UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -92,7 +92,7 @@ avatar = Table(
|
|||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
Column("defaultHave", Boolean, server_default="0"),
|
||||||
Column("sortName", String(255)),
|
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"),
|
UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -108,7 +108,7 @@ nameplate = Table(
|
|||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
Column("defaultHave", Boolean, server_default="0"),
|
||||||
Column("sortName", String(255)),
|
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"),
|
UniqueConstraint("version", "nameplateId", name="chuni_static_nameplate_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ character = Table(
|
|||||||
Column("imagePath3", String(255)),
|
Column("imagePath3", String(255)),
|
||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
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"),
|
UniqueConstraint("version", "characterId", name="chuni_static_character_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -143,7 +143,7 @@ trophy = Table(
|
|||||||
Column("rareType", Integer),
|
Column("rareType", Integer),
|
||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
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"),
|
UniqueConstraint("version", "trophyId", name="chuni_static_trophy_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -159,7 +159,7 @@ map_icon = Table(
|
|||||||
Column("iconPath", String(255)),
|
Column("iconPath", String(255)),
|
||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
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"),
|
UniqueConstraint("version", "mapIconId", name="chuni_static_mapicon_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -175,7 +175,7 @@ system_voice = Table(
|
|||||||
Column("imagePath", String(255)),
|
Column("imagePath", String(255)),
|
||||||
Column("isEnabled", Boolean, server_default="1"),
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
Column("defaultHave", Boolean, server_default="0"),
|
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"),
|
UniqueConstraint("version", "voiceId", name="chuni_static_systemvoice_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -197,7 +197,7 @@ gachas = Table(
|
|||||||
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
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("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||||
Column("noticeEndDate", TIMESTAMP, server_default="2038-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"),
|
UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -218,7 +218,7 @@ cards = Table(
|
|||||||
Column("combo", Integer, nullable=False),
|
Column("combo", Integer, nullable=False),
|
||||||
Column("chain", Integer, nullable=False),
|
Column("chain", Integer, nullable=False),
|
||||||
Column("skillName", String(255), 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"),
|
UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -304,7 +304,45 @@ unlock_challenge = Table(
|
|||||||
Column("courseId4", Integer),
|
Column("courseId4", Integer),
|
||||||
Column("courseId5", Integer),
|
Column("courseId5", Integer),
|
||||||
UniqueConstraint(
|
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",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@@ -1232,3 +1270,108 @@ 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()
|
||||||
|
|
||||||
|
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>
|
<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>
|
</tr>
|
||||||
{% endif %}
|
{% 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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +117,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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="col-lg-8 m-auto mt-3">
|
||||||
<div class="card bg-card rounded">
|
<div class="card bg-card rounded">
|
||||||
<table class="table-large table-rowdistinct">
|
<table class="table-large table-rowdistinct">
|
||||||
@@ -201,6 +222,10 @@ items = {
|
|||||||
"map-icon": ["{{ map_icons|length }}", "{{ profile.mapIconId }}"],
|
"map-icon": ["{{ map_icons|length }}", "{{ profile.mapIconId }}"],
|
||||||
"system-voice":["{{ system_voices|length }}", "{{ profile.voiceId }}"]
|
"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);
|
types = Object.keys(items);
|
||||||
|
|
||||||
function changeItem(type, id, name) {
|
function changeItem(type, id, name) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from titles.chuni.const import (
|
|||||||
MapAreaConditionLogicalOperator,
|
MapAreaConditionLogicalOperator,
|
||||||
MapAreaConditionType,
|
MapAreaConditionType,
|
||||||
)
|
)
|
||||||
|
from titles.chuni.luminous import MysticAreaConditions
|
||||||
from titles.chuni.luminousplus import ChuniLuminousPlus
|
from titles.chuni.luminousplus import ChuniLuminousPlus
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,38 @@ class ChuniVerse(ChuniLuminousPlus):
|
|||||||
# Does CARD MAKER 1.35 work this far up?
|
# Does CARD MAKER 1.35 work this far up?
|
||||||
user_data["lastDataVersion"] = "2.30.00"
|
user_data["lastDataVersion"] = "2.30.00"
|
||||||
return user_data
|
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:
|
async def handle_get_game_course_level_api_request(self, data: Dict) -> Dict:
|
||||||
unlock_challenges = await self.data.static.get_unlock_challenges(self.version)
|
unlock_challenges = await self.data.static.get_unlock_challenges(self.version)
|
||||||
@@ -81,9 +114,10 @@ class ChuniVerse(ChuniLuminousPlus):
|
|||||||
|
|
||||||
unlock_condition = conditions.get(
|
unlock_condition = conditions.get(
|
||||||
unlock_challenge_id,
|
unlock_challenge_id,
|
||||||
|
# default is to unlock for players above 5.00 rating
|
||||||
{
|
{
|
||||||
"type": MapAreaConditionType.TROPHY_OBTAINED.value, # always unlocked
|
"type": MapAreaConditionType.MINIMUM_RATING.value,
|
||||||
"conditionId": 0,
|
"conditionId": 500,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,7 +207,7 @@ class ChuniVerse(ChuniLuminousPlus):
|
|||||||
|
|
||||||
user_rec_music_list = [
|
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
|
# 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?!
|
# for some reason, not all music ids are shown in game?!
|
||||||
"recMusicList": ";".join(
|
"recMusicList": ";".join(
|
||||||
@@ -193,7 +227,8 @@ class ChuniVerse(ChuniLuminousPlus):
|
|||||||
class UserRecRating:
|
class UserRecRating:
|
||||||
ratingMin: int
|
ratingMin: int
|
||||||
ratingMax: 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
|
recMusicList: str
|
||||||
|
|
||||||
length: int
|
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