From 9bc18f179dbee153116444000a44ffb887128b80 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 12:37:43 +0800 Subject: [PATCH 01/63] add prism+ consts and cm support --- titles/cm/read.py | 3 ++- titles/mai2/const.py | 4 +++- titles/mai2/index.py | 16 +++++++++++----- titles/mai2/prismplus.py | 22 ++++++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 titles/mai2/prismplus.py diff --git a/titles/cm/read.py b/titles/cm/read.py index cf697c6..f4617e5 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -208,7 +208,8 @@ class CardMakerReader(BaseReader): "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, "1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - "1.50": Mai2Constants.VER_MAIMAI_DX_PRISM + "1.50": Mai2Constants.VER_MAIMAI_DX_PRISM, + "1.55": Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS } for root, dirs, files in os.walk(base_dir): diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 1d6a4dd..50d7b17 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -57,6 +57,7 @@ class Mai2Constants: VER_MAIMAI_DX_BUDDIES = 21 VER_MAIMAI_DX_BUDDIES_PLUS = 22 VER_MAIMAI_DX_PRISM = 23 + VER_MAIMAI_DX_PRISM_PLUS = 24 VERSION_STRING = ( "maimai", @@ -82,7 +83,8 @@ class Mai2Constants: "maimai DX FESTiVAL PLUS", "maimai DX BUDDiES", "maimai DX BUDDiES PLUS", - "maimai DX PRiSM" + "maimai DX PRiSM", + "maimai DX PRiSM PLUS" ) @classmethod diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 86923e7..1f0a6be 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -32,6 +32,7 @@ from .festivalplus import Mai2FestivalPlus from .buddies import Mai2Buddies from .buddiesplus import Mai2BuddiesPlus from .prism import Mai2Prism +from .prismplus import Mai2PrismPlus class Mai2Servlet(BaseServlet): @@ -68,7 +69,8 @@ class Mai2Servlet(BaseServlet): Mai2FestivalPlus, Mai2Buddies, Mai2BuddiesPlus, - Mai2Prism + Mai2Prism, + Mai2PrismPlus ] self.logger = logging.getLogger("mai2") @@ -308,10 +310,12 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS elif version >= 140 and version < 145: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >= 145 and version <150: # BUDDiES PLUS + elif version >= 145 and version < 150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - elif version >=150: + elif version >= 150 and version < 155: internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif version >= 155: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS elif game_code == "SDGA": # Int if version < 105: # 1.0 @@ -332,10 +336,12 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS elif version >= 140 and version < 145: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >= 145 and version <150: # BUDDiES PLUS + elif version >= 145 and version < 150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - elif version >=150: + elif version >= 150 and version < 155: internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif version >= 155: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS 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 diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py new file mode 100644 index 0000000..296207c --- /dev/null +++ b/titles/mai2/prismplus.py @@ -0,0 +1,22 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.prism import Mai2Prism +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + + +class Mai2PrismPlus(Mai2Prism): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.55.00" + return user_data + + From f8888c2392acc485c611d835b0e4883377076f92 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 12:42:40 +0800 Subject: [PATCH 02/63] add prism+ playlog support --- ...10616ba4_mai2_add_prism_playlog_support.py | 28 +++++++++++++++++++ titles/mai2/schema/score.py | 1 + 2 files changed, 29 insertions(+) create mode 100644 core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py diff --git a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py new file mode 100644 index 0000000..49eab78 --- /dev/null +++ b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py @@ -0,0 +1,28 @@ +"""Mai2 add PRiSM+ playlog support + +Revision ID: bdf710616ba4 +Revises: 16f34bf7b968 +Create Date: 2025-04-02 12:42:08.981516 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bdf710616ba4' +down_revision = '16f34bf7b968' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('mai2_playlog', sa.Column('extBool3', sa.Boolean(), nullable=True,server_default=sa.text("NULL"))) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('mai2_playlog', 'extBool3') + # ### end Alembic commands ### diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index f3e7002..06e686b 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -149,6 +149,7 @@ playlog = Table( Column("extNum4", Integer), Column("extBool1", Boolean), # new with buddies Column("extBool2", Boolean), # new with prism + Column("extBool3", Boolean), # new with prism+ Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) From 4ceac7db3547f1660e453b737c1037dd559e7396 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 13:49:46 +0800 Subject: [PATCH 03/63] add clientplaytimeapi support --- titles/mai2/prismplus.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py index 296207c..ff9c5fc 100644 --- a/titles/mai2/prismplus.py +++ b/titles/mai2/prismplus.py @@ -19,4 +19,8 @@ class Mai2PrismPlus(Mai2Prism): user_data["lastDataVersion"] = "1.55.00" return user_data - + async def handle_upsert_client_play_time_api_request(self, data: Dict) -> Dict: + return{ + "returnCode": 1, + "apiName": "UpsertClientPlayTimeApi" + } From 756c7ce951aa96302ea2b0c077150c39797c1a91 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 13:56:56 +0800 Subject: [PATCH 04/63] update readme.md and game_specific_info.md --- docs/game_specific_info.md | 52 +++++++++++++++++++------------------- readme.md | 1 + 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 6bb3f67..052498d 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -203,32 +203,32 @@ Presents are items given to the user when they login, with a little animation (f ### Versions | Game Code | Version ID | Version Name | -|-----------|------------|-------------------------| -| SBXL | 0 | maimai | -| SBXL | 1 | maimai PLUS | -| SBZF | 2 | maimai GreeN | -| SBZF | 3 | maimai GreeN PLUS | -| SDBM | 4 | maimai ORANGE | -| SDBM | 5 | maimai ORANGE PLUS | -| SDCQ | 6 | maimai PiNK | -| SDCQ | 7 | maimai PiNK PLUS | -| SDDK | 8 | maimai MURASAKi | -| SDDK | 9 | maimai MURASAKi PLUS | -| SDDZ | 10 | maimai MiLK | -| SDDZ | 11 | maimai MiLK PLUS | -| SDEY | 12 | maimai FiNALE | -| SDEZ | 13 | maimai DX | -| SDEZ | 14 | maimai DX PLUS | -| SDEZ | 15 | maimai DX Splash | -| SDEZ | 16 | maimai DX Splash PLUS | -| SDEZ | 17 | maimai DX UNiVERSE | -| SDEZ | 18 | maimai DX UNiVERSE PLUS | -| SDEZ | 19 | maimai DX FESTiVAL | -| SDEZ | 20 | maimai DX FESTiVAL PLUS | -| SDEZ | 21 | maimai DX BUDDiES | -| SDEZ | 22 | maimai DX BUDDiES PLUS | -| SDEZ | 23 | maimai DX PRiSM | - +|----------|------------|-------------------------| +| SBXL | 0 | maimai | +| SBXL | 1 | maimai PLUS | +| SBZF | 2 | maimai GreeN | +| SBZF | 3 | maimai GreeN PLUS | +| SDBM | 4 | maimai ORANGE | +| SDBM | 5 | maimai ORANGE PLUS | +| SDCQ | 6 | maimai PiNK | +| SDCQ | 7 | maimai PiNK PLUS | +| SDDK | 8 | maimai MURASAKi | +| SDDK | 9 | maimai MURASAKi PLUS | +| SDDZ | 10 | maimai MiLK | +| SDDZ | 11 | maimai MiLK PLUS | +| SDEY | 12 | maimai FiNALE | +| SDEZ | 13 | maimai DX | +| SDEZ | 14 | maimai DX PLUS | +| SDEZ | 15 | maimai DX Splash | +| SDEZ | 16 | maimai DX Splash PLUS | +| SDEZ | 17 | maimai DX UNiVERSE | +| SDEZ | 18 | maimai DX UNiVERSE PLUS | +| SDEZ | 19 | maimai DX FESTiVAL | +| SDEZ | 20 | maimai DX FESTiVAL PLUS | +| SDEZ | 21 | maimai DX BUDDiES | +| SDEZ | 22 | maimai DX BUDDiES PLUS | +| SDEZ | 23 | maimai DX PRiSM | +| SDEZ | 24 | maimai DX PRiSM PLUS | ### Importer diff --git a/readme.md b/readme.md index e29784d..663b7cd 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + BUDDiES + BUDDiES PLUS + PRiSM + + PRiSM PLUS + O.N.G.E.K.I. + SUMMER From c0df7cd084e61b9ecbc86419f58a3365440200a6 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 4 Apr 2025 09:10:41 +0800 Subject: [PATCH 05/63] database support for prism kaleidxScope Key Condition store --- ....py => 5d7b38996e67_mai2_prism_support.py} | 29 +++++++++---- titles/mai2/prism.py | 16 +++++++ titles/mai2/read.py | 34 ++++++++++++++- titles/mai2/schema/static.py | 43 +++++++++++++++++++ 4 files changed, 113 insertions(+), 9 deletions(-) rename core/data/alembic/versions/{16f34bf7b968_mai2_kaleidx_scope_support.py => 5d7b38996e67_mai2_prism_support.py} (61%) diff --git a/core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py b/core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py similarity index 61% rename from core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py rename to core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py index b8baa1a..64416ae 100644 --- a/core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py +++ b/core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py @@ -1,8 +1,8 @@ -"""Mai2 Kaleidx Scope Support +"""Mai2 PRiSM support -Revision ID: 16f34bf7b968 -Revises: d0f1c7fa9505 -Create Date: 2025-04-02 07:06:15.829591 +Revision ID: 5d7b38996e67 +Revises: 1d0014d35220 +Create Date: 2025-04-04 06:28:24.898912 """ from alembic import op @@ -10,15 +10,25 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '16f34bf7b968' -down_revision = 'd0f1c7fa9505' +revision = '5d7b38996e67' +down_revision = '1d0014d35220' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('mai2_score_kaleidx_scope', + op.create_table('mai2_static_kaleidxscope_condition', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('conditionId', sa.Integer(), nullable=True), + sa.Column('conditionName', sa.String(length=255), nullable=True), + sa.Column('songId', sa.Integer(), nullable=True), + sa.Column('songName', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('conditionId', 'conditionName', 'songId', 'songName', name='mai2_static_kaleidxscope_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('mai2_score_kaleidxscope', sa.Column('id', sa.Integer(), nullable=False), sa.Column('user', sa.Integer(), nullable=False), sa.Column('gateId', sa.Integer(), nullable=True), @@ -41,10 +51,13 @@ def upgrade(): sa.UniqueConstraint('user', 'gateId', name='mai2_score_best_uk'), mysql_charset='utf8mb4' ) + op.add_column('mai2_playlog', sa.Column('extBool2', sa.Boolean(), nullable=True, server_default=sa.text("NULL"))) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('mai2_score_kaleidx_scope') + op.drop_column('mai2_playlog', 'extBool2') + op.drop_table('mai2_score_kaleidxscope') + op.drop_table('mai2_static_kaleidxscope_condition') # ### end Alembic commands ### diff --git a/titles/mai2/prism.py b/titles/mai2/prism.py index 5db7c8a..409132c 100644 --- a/titles/mai2/prism.py +++ b/titles/mai2/prism.py @@ -49,6 +49,22 @@ class Mai2Prism(Mai2BuddiesPlus): } async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: + # kaleidxscope keyget condition judgement + # player may get key before GateFound + for gate in range(1,7): + condition_list = await self.data.static.get_kaleidxscope_condition(gate) + if not condition_list: + continue + condition_satisfy = 0 + for condition in condition_list: + score_list = await self.data.score.get_best_scores(user_id=data["userId"], song_id=condition[3]) + if score_list: + condition_satisfy = condition_satisfy + 1 + if len(condition_list) == condition_satisfy: + new_kaleidxscope = {'gateId': gate, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"]) if kaleidxscope is None: diff --git a/titles/mai2/read.py b/titles/mai2/read.py index d9450ac..7bbd33f 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -48,8 +48,12 @@ class Mai2Reader(BaseReader): self.logger.info(f"Read from {dir}") await self.get_events(f"{dir}/event") await self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") - await self.read_music(f"{dir}/music") + #await self.read_music(f"{dir}/music") await self.read_tickets(f"{dir}/ticket") + + if self.version >= Mai2Constants.VER_MAIMAI_DX_PRISM: + for dir in data_dirs: + await self.read_kaleidxscope_condition(f"{dir}/kaleidxScopeKeyCondition") else: if not os.path.exists(f"{self.bin_dir}/tables"): @@ -341,3 +345,31 @@ class Mai2Reader(BaseReader): if scores is None or text is None: return # TODO + + + async def read_kaleidxscope_condition(self, base_dir: str) -> None : + self.logger.info(f"Reading KaleidxScope Key Conditions from {base_dir}...") + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/KaleidxScopeKeyCondition.xml"): + with open(f"{root}/{dir}/KaleidxScopeKeyCondition.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + condition_id = int(troot.find("name").find("id").text) + condition_name = troot.find("name").find("str").text + + music_list = troot.find("musicIds").find("list") + for music in music_list.findall("StringID"): + music_id = int(music.find("id").text) + music_name = music.find("str").text + + await self.data.static.put_kaleidxscope_condition( + condition_id, + condition_name, + music_id, + music_name + ) + self.logger.info( + f"Add music {music_id} for condition {condition_id}" + ) \ No newline at end of file diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index ddba0f8..3fe6391 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -71,6 +71,17 @@ cards = Table( mysql_charset="utf8mb4", ) +kaleidxscope_condition = Table( + "mai2_static_kaleidxscope_condition", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("conditionId", Integer), + Column("conditionName", String(255)), + Column("songId", Integer), + Column("songName", String(255)), + UniqueConstraint("conditionId", "conditionName", "songId", "songName", name="mai2_static_kaleidxscope_uk"), + mysql_charset="utf8mb4", +) class Mai2StaticData(BaseData): async def put_game_event( @@ -264,3 +275,35 @@ class Mai2StaticData(BaseData): result = await self.execute(event.update(event.c.id == table_id).values(enabled=is_enable, startDate = start_date)) if not result: self.logger.error(f"Failed to update event {table_id} - {is_enable} {start_date}") + + # new in prism + async def put_kaleidxscope_condition( + self, + condition_id: int, + condition_name: str, + music_id: int, + music_name: str + ) -> Optional[int]: + sql = insert(kaleidxscope_condition).values( + conditionId = condition_id, + conditionName = condition_name, + songId = music_id, + songName = music_name, + ) + + + conflict = sql.on_duplicate_key_update(conditionName=condition_name, songName=music_name) + + result = await self.execute(conflict) + if result is None: + self.logger.warning( + f"put_kaleidxscope_condition: Failed to insert kaleidxScope Key Condition! conditionID {condition_id} songId {music_id}" + ) + return result.lastrowid + + async def get_kaleidxscope_condition(self, condition_id: int) -> None: + sql = kaleidxscope_condition.select(kaleidxscope_condition.c.conditionId == condition_id) + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() \ No newline at end of file From dd10508e68ae33ea8c05f647c5af8605f114f13d Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 4 Apr 2025 09:12:08 +0800 Subject: [PATCH 06/63] unused database deleted --- .../d0f1c7fa9505_mai2_add_prism_support.py | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py diff --git a/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py b/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py deleted file mode 100644 index c879706..0000000 --- a/core/data/alembic/versions/d0f1c7fa9505_mai2_add_prism_support.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Mai2 add PRiSM support - -Revision ID: d0f1c7fa9505 -Revises: 1d0014d35220 -Create Date: 2025-04-02 06:37:10.657372 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd0f1c7fa9505' -down_revision = '1d0014d35220' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('mai2_playlog', sa.Column('extBool2', sa.Boolean(), nullable=True,server_default=sa.text("NULL"))) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('mai2_playlog', 'extBool2') - # ### end Alembic commands ### From 3b7a577ea208a0b01532eb457a537615cf4e3839 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 12:37:43 +0800 Subject: [PATCH 07/63] add prism+ consts and cm support --- titles/cm/read.py | 3 ++- titles/mai2/const.py | 4 +++- titles/mai2/index.py | 16 +++++++++++----- titles/mai2/prismplus.py | 22 ++++++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 titles/mai2/prismplus.py diff --git a/titles/cm/read.py b/titles/cm/read.py index cf697c6..f4617e5 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -208,7 +208,8 @@ class CardMakerReader(BaseReader): "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, "1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - "1.50": Mai2Constants.VER_MAIMAI_DX_PRISM + "1.50": Mai2Constants.VER_MAIMAI_DX_PRISM, + "1.55": Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS } for root, dirs, files in os.walk(base_dir): diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 1d6a4dd..50d7b17 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -57,6 +57,7 @@ class Mai2Constants: VER_MAIMAI_DX_BUDDIES = 21 VER_MAIMAI_DX_BUDDIES_PLUS = 22 VER_MAIMAI_DX_PRISM = 23 + VER_MAIMAI_DX_PRISM_PLUS = 24 VERSION_STRING = ( "maimai", @@ -82,7 +83,8 @@ class Mai2Constants: "maimai DX FESTiVAL PLUS", "maimai DX BUDDiES", "maimai DX BUDDiES PLUS", - "maimai DX PRiSM" + "maimai DX PRiSM", + "maimai DX PRiSM PLUS" ) @classmethod diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 86923e7..1f0a6be 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -32,6 +32,7 @@ from .festivalplus import Mai2FestivalPlus from .buddies import Mai2Buddies from .buddiesplus import Mai2BuddiesPlus from .prism import Mai2Prism +from .prismplus import Mai2PrismPlus class Mai2Servlet(BaseServlet): @@ -68,7 +69,8 @@ class Mai2Servlet(BaseServlet): Mai2FestivalPlus, Mai2Buddies, Mai2BuddiesPlus, - Mai2Prism + Mai2Prism, + Mai2PrismPlus ] self.logger = logging.getLogger("mai2") @@ -308,10 +310,12 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS elif version >= 140 and version < 145: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >= 145 and version <150: # BUDDiES PLUS + elif version >= 145 and version < 150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - elif version >=150: + elif version >= 150 and version < 155: internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif version >= 155: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS elif game_code == "SDGA": # Int if version < 105: # 1.0 @@ -332,10 +336,12 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS elif version >= 140 and version < 145: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >= 145 and version <150: # BUDDiES PLUS + elif version >= 145 and version < 150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, - elif version >=150: + elif version >= 150 and version < 155: internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif version >= 155: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS 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 diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py new file mode 100644 index 0000000..296207c --- /dev/null +++ b/titles/mai2/prismplus.py @@ -0,0 +1,22 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.prism import Mai2Prism +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + + +class Mai2PrismPlus(Mai2Prism): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.55.00" + return user_data + + From d598c8fba0867dedddd2717895c40a5a38a7d0ce Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 12:42:40 +0800 Subject: [PATCH 08/63] add prism+ playlog support --- ...10616ba4_mai2_add_prism_playlog_support.py | 28 +++++++++++++++++++ titles/mai2/schema/score.py | 1 + 2 files changed, 29 insertions(+) create mode 100644 core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py diff --git a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py new file mode 100644 index 0000000..49eab78 --- /dev/null +++ b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py @@ -0,0 +1,28 @@ +"""Mai2 add PRiSM+ playlog support + +Revision ID: bdf710616ba4 +Revises: 16f34bf7b968 +Create Date: 2025-04-02 12:42:08.981516 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bdf710616ba4' +down_revision = '16f34bf7b968' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('mai2_playlog', sa.Column('extBool3', sa.Boolean(), nullable=True,server_default=sa.text("NULL"))) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('mai2_playlog', 'extBool3') + # ### end Alembic commands ### diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index d03dba4..54e9287 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -149,6 +149,7 @@ playlog = Table( Column("extNum4", Integer), Column("extBool1", Boolean), # new with buddies Column("extBool2", Boolean), # new with prism + Column("extBool3", Boolean), # new with prism+ Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) From a4bcca9171135897d4cc2c310db44dfc94bef0da Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 13:49:46 +0800 Subject: [PATCH 09/63] add clientplaytimeapi support --- titles/mai2/prismplus.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py index 296207c..ff9c5fc 100644 --- a/titles/mai2/prismplus.py +++ b/titles/mai2/prismplus.py @@ -19,4 +19,8 @@ class Mai2PrismPlus(Mai2Prism): user_data["lastDataVersion"] = "1.55.00" return user_data - + async def handle_upsert_client_play_time_api_request(self, data: Dict) -> Dict: + return{ + "returnCode": 1, + "apiName": "UpsertClientPlayTimeApi" + } From 134af15ed73335b2166b6f4bea6f758506e39dea Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Wed, 2 Apr 2025 13:56:56 +0800 Subject: [PATCH 10/63] update readme.md and game_specific_info.md --- docs/game_specific_info.md | 52 +++++++++++++++++++------------------- readme.md | 1 + 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 6bb3f67..052498d 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -203,32 +203,32 @@ Presents are items given to the user when they login, with a little animation (f ### Versions | Game Code | Version ID | Version Name | -|-----------|------------|-------------------------| -| SBXL | 0 | maimai | -| SBXL | 1 | maimai PLUS | -| SBZF | 2 | maimai GreeN | -| SBZF | 3 | maimai GreeN PLUS | -| SDBM | 4 | maimai ORANGE | -| SDBM | 5 | maimai ORANGE PLUS | -| SDCQ | 6 | maimai PiNK | -| SDCQ | 7 | maimai PiNK PLUS | -| SDDK | 8 | maimai MURASAKi | -| SDDK | 9 | maimai MURASAKi PLUS | -| SDDZ | 10 | maimai MiLK | -| SDDZ | 11 | maimai MiLK PLUS | -| SDEY | 12 | maimai FiNALE | -| SDEZ | 13 | maimai DX | -| SDEZ | 14 | maimai DX PLUS | -| SDEZ | 15 | maimai DX Splash | -| SDEZ | 16 | maimai DX Splash PLUS | -| SDEZ | 17 | maimai DX UNiVERSE | -| SDEZ | 18 | maimai DX UNiVERSE PLUS | -| SDEZ | 19 | maimai DX FESTiVAL | -| SDEZ | 20 | maimai DX FESTiVAL PLUS | -| SDEZ | 21 | maimai DX BUDDiES | -| SDEZ | 22 | maimai DX BUDDiES PLUS | -| SDEZ | 23 | maimai DX PRiSM | - +|----------|------------|-------------------------| +| SBXL | 0 | maimai | +| SBXL | 1 | maimai PLUS | +| SBZF | 2 | maimai GreeN | +| SBZF | 3 | maimai GreeN PLUS | +| SDBM | 4 | maimai ORANGE | +| SDBM | 5 | maimai ORANGE PLUS | +| SDCQ | 6 | maimai PiNK | +| SDCQ | 7 | maimai PiNK PLUS | +| SDDK | 8 | maimai MURASAKi | +| SDDK | 9 | maimai MURASAKi PLUS | +| SDDZ | 10 | maimai MiLK | +| SDDZ | 11 | maimai MiLK PLUS | +| SDEY | 12 | maimai FiNALE | +| SDEZ | 13 | maimai DX | +| SDEZ | 14 | maimai DX PLUS | +| SDEZ | 15 | maimai DX Splash | +| SDEZ | 16 | maimai DX Splash PLUS | +| SDEZ | 17 | maimai DX UNiVERSE | +| SDEZ | 18 | maimai DX UNiVERSE PLUS | +| SDEZ | 19 | maimai DX FESTiVAL | +| SDEZ | 20 | maimai DX FESTiVAL PLUS | +| SDEZ | 21 | maimai DX BUDDiES | +| SDEZ | 22 | maimai DX BUDDiES PLUS | +| SDEZ | 23 | maimai DX PRiSM | +| SDEZ | 24 | maimai DX PRiSM PLUS | ### Importer diff --git a/readme.md b/readme.md index e29784d..663b7cd 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + BUDDiES + BUDDiES PLUS + PRiSM + + PRiSM PLUS + O.N.G.E.K.I. + SUMMER From 933d8bea2100ed162a13617ffbc1f3f466291576 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 15 May 2025 02:44:37 +0800 Subject: [PATCH 11/63] add import --- titles/mai2/index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index acaa2ca..e5b3c24 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -32,6 +32,7 @@ from .festivalplus import Mai2FestivalPlus from .buddies import Mai2Buddies from .buddiesplus import Mai2BuddiesPlus from .prism import Mai2Prism +from .prismplus import Mai2PrismPlus class Mai2Servlet(BaseServlet): From 394ec74fb7008b9d90124a45b6590c8c175b6867 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 15 May 2025 03:14:24 +0800 Subject: [PATCH 12/63] add key unlock condition define --- titles/mai2/const.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 85d4845..d8b44dc 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -95,9 +95,13 @@ class Mai2Constants: 2: [11102, 11234, 11300, 11529, 11542, 11612], #白の扉: set Frame as "Latent Kingdom" (459504), play 3 or 4 songs by the composer 大国奏音 in 1 pc 3: [], - #紫の扉: need to enter redeem code 51090942171709440000 + #紫の扉: JP: need to enter redeem code 51090942171709440000 4: [11023, 11106, 11221, 11222, 11300, 11374, 11458, 11523, 11619, 11663, 11746], - #青の扉: Played 11 songs + #黑の扉: Played 11 songs + 5: [11003, 11095, 11152, 11224, 11296, 11375, 11452, 11529, 11608, 11669, 11736, 11806], + #黄の扉: Use random selection to play one of the songs + 6: [212, 213, 337, 270, 271, 11504, 339, 453, 11336, 11852] + #赤の扉: Played 10 songs } MAI_VERSION_LUT = { "100": VER_MAIMAI, From 8b050e89eb256ed5d887f7d994ada7172e32198e Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 15 May 2025 03:53:42 +0800 Subject: [PATCH 13/63] fix conflict mark --- .../5cf98cfe52ad_mai2_prism_support.py | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py b/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py index cf4287c..d3296cd 100644 --- a/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py +++ b/core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py @@ -1,14 +1,8 @@ """Mai2 PRiSM support -<<<<<<<< HEAD:core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py -Revision ID: 5d7b38996e67 -Revises: 1d0014d35220 -Create Date: 2025-04-04 06:28:24.898912 -======== Revision ID: 5cf98cfe52ad Revises: 263884e774cc Create Date: 2025-04-08 08:00:51.243089 ->>>>>>>> refs/heads/develop:core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py """ from alembic import op @@ -16,32 +10,15 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -<<<<<<<< HEAD:core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py -revision = '5d7b38996e67' -down_revision = '1d0014d35220' -======== + revision = '5cf98cfe52ad' down_revision = '263884e774cc' ->>>>>>>> refs/heads/develop:core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### -<<<<<<<< HEAD:core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py - op.create_table('mai2_static_kaleidxscope_condition', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('conditionId', sa.Integer(), nullable=True), - sa.Column('conditionName', sa.String(length=255), nullable=True), - sa.Column('songId', sa.Integer(), nullable=True), - sa.Column('songName', sa.String(length=255), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('conditionId', 'conditionName', 'songId', 'songName', name='mai2_static_kaleidxscope_uk'), - mysql_charset='utf8mb4' - ) -======== ->>>>>>>> refs/heads/develop:core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py op.create_table('mai2_score_kaleidxscope', sa.Column('id', sa.Integer(), nullable=False), sa.Column('user', sa.Integer(), nullable=False), @@ -73,8 +50,4 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_column('mai2_playlog', 'extBool2') op.drop_table('mai2_score_kaleidxscope') -<<<<<<<< HEAD:core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py - op.drop_table('mai2_static_kaleidxscope_condition') -======== ->>>>>>>> refs/heads/develop:core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py # ### end Alembic commands ### From 611806828a42f4a2559170b0d987249e7d312619 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 15 May 2025 08:19:42 +0800 Subject: [PATCH 14/63] add Gate 5,6 judgement --- ...f710616ba4_mai2_add_prism_plus_support.py} | 3 +- titles/mai2/prismplus.py | 86 +++++++++++++++++++ titles/mai2/schema/score.py | 4 +- 3 files changed, 90 insertions(+), 3 deletions(-) rename core/data/alembic/versions/{bdf710616ba4_mai2_add_prism_playlog_support.py => bdf710616ba4_mai2_add_prism_plus_support.py} (90%) diff --git a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py similarity index 90% rename from core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py rename to core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py index 49eab78..3620b9d 100644 --- a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py +++ b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py @@ -11,7 +11,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'bdf710616ba4' -down_revision = '16f34bf7b968' +#TODO: change down_revision when final merge +down_revision = 'f6007bbf057d' branch_labels = None depends_on = None diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py index ff9c5fc..1719189 100644 --- a/titles/mai2/prismplus.py +++ b/titles/mai2/prismplus.py @@ -24,3 +24,89 @@ class Mai2PrismPlus(Mai2Prism): "returnCode": 1, "apiName": "UpsertClientPlayTimeApi" } + async def handle_get_game_kaleidx_scope_api_request(self, data: Dict) -> Dict: + return { + "gameKaleidxScopeList": [ + {"gateId": 1, "phaseId": 6}, + {"gateId": 2, "phaseId": 6}, + {"gateId": 3, "phaseId": 6}, + {"gateId": 4, "phaseId": 6}, + {"gateId": 5, "phaseId": 6}, + {"gateId": 6, "phaseId": 6}, + {"gateId": 7, "phaseId": 6} + ] + } + + async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: + # kaleidxscope keyget condition judgement + # player may get key before GateFound + for gate in range(1,8): + if gate == 1 or gate == 4 or gate == 6: + condition_satisfy = 0 + for condition in Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]: + score_list = await self.data.score.get_best_scores(user_id=data["userId"], song_id=condition) + if score_list: + condition_satisfy = condition_satisfy + 1 + if len(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]) == condition_satisfy: + new_kaleidxscope = {'gateId': gate, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + elif gate == 2: + user_profile = await self.data.profile.get_profile_detail(user_id=data["userId"], version=self.version) + user_frame = user_profile["frameId"] + if user_frame == 459504: + playlogs = await self.data.score.get_playlogs(user_id=data["userId"], idx=0, limit=0) + + playlog_dict = {} + for playlog in playlogs: + playlog_id = playlog["playlogId"] + if playlog_id not in playlog_dict: + playlog_dict[playlog_id] = [] + playlog_dict[playlog_id].append(playlog["musicId"]) + valid_playlogs = [] + allowed_music = set(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[2]) + for playlog_id, music_ids in playlog_dict.items(): + + if len(music_ids) != len(set(music_ids)): + continue + all_valid = True + for mid in music_ids: + if mid not in allowed_music: + all_valid = False + break + if all_valid: + valid_playlogs.append(playlog_id) + + if valid_playlogs: + new_kaleidxscope = {'gateId': 2, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + + elif gate == 5: + + playlogs = await self.data.score.get_playlogs(user_id=data["userId"], idx=0, limit=0) + allowed_music = set(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[5]) + valid_playlogs = [] + + for playlog in playlogs: + if playlog["extBool2"] == 1 and playlog["musicId"] in allowed_music: + valid_playlogs.append(playlog["playlogId"]) # 直接记录 playlogId + if valid_playlogs: + new_kaleidxscope = {'gateId': 5, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"]) + + if kaleidxscope is None: + return {"userId": data["userId"], "userKaleidxScopeList":[]} + + kaleidxscope_list = [] + for kaleidxscope_data in kaleidxscope: + tmp = kaleidxscope_data._asdict() + tmp.pop("user") + tmp.pop("id") + kaleidxscope_list.append(tmp) + return { + "userId": data["userId"], + "userKaleidxScopeList": kaleidxscope_list + } \ No newline at end of file diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 54e9287..804cbaa 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -148,8 +148,8 @@ playlog = Table( Column("extNum2", Integer), Column("extNum4", Integer), Column("extBool1", Boolean), # new with buddies - Column("extBool2", Boolean), # new with prism - Column("extBool3", Boolean), # new with prism+ + Column("extBool2", Boolean), # new with prism IsRandomSelect + Column("extBool3", Boolean), # new with prism+ IsTrackSkip Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) From 5e5365d22b817e08d496201540266b5044754b48 Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Sat, 31 May 2025 07:15:35 +1200 Subject: [PATCH 15/63] Allnet Lite Power On Support --- core/allnet.py | 106 +++++++++++++++++++++++++++++++++++---- core/app.py | 1 + core/config.py | 5 ++ example_config/core.yaml | 1 + 4 files changed, 103 insertions(+), 10 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 0912f56..2115b9a 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -7,6 +7,7 @@ import logging import coloredlogs import urllib.parse import math +import random from typing import Dict, List, Any, Optional, Union, Final from logging.handlers import TimedRotatingFileHandler from starlette.requests import Request @@ -17,7 +18,10 @@ from datetime import datetime from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad from Crypto.Signature import PKCS1_v1_5 +import os from os import path, environ, mkdir, access, W_OK from .config import CoreConfig @@ -132,12 +136,20 @@ class AllnetServlet: async def handle_poweron(self, request: Request): request_ip = Utils.get_ip_addr(request) pragma_header = request.headers.get('Pragma', "") + useragent_header = request.headers.get('User-Agent', "") is_dfi = pragma_header == "DFI" + is_lite = useragent_header[5:] == "Windows/Lite" data = await request.body() + + if not self.config.allnet.allnet_lite_key and is_lite: + self.logger.error("!!!LITE KEY NOT SET!!!") + os._exit(1) try: if is_dfi: req_urlencode = self.from_dfi(data) + elif is_lite: + req_urlencode = self.dec_lite(self.config.allnet.allnet_lite_key, data[:16], data) else: req_urlencode = data @@ -145,20 +157,30 @@ class AllnetServlet: if req_dict is None: raise AllnetRequestException() - req = AllnetPowerOnRequest(req_dict[0]) + if is_lite: + req = AllnetPowerOnRequestLite(req_dict[0]) + else: + req = AllnetPowerOnRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver: + if not req.game_id or not req.ver or not req.serial or not req.token and is_lite: raise AllnetRequestException( f"Bad auth request params from {request_ip} - {vars(req)}" ) + elif not is_lite: + if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver: + raise AllnetRequestException( + f"Bad auth request params from {request_ip} - {vars(req)}" + ) except AllnetRequestException as e: if e.message != "": self.logger.error(e) return PlainTextResponse() - if req.format_ver == 3: + if is_lite: + resp = AllnetPowerOnResponseLite(req.token) + elif req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) elif req.format_ver == 2: resp = AllnetPowerOnResponse2() @@ -175,11 +197,14 @@ class AllnetServlet: ) self.logger.warning(msg) - resp.stat = ALLNET_STAT.bad_machine.value + if is_lite: + resp.result = ALLNET_STAT.bad_machine.value + else: + resp.stat = ALLNET_STAT.bad_machine.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") - if machine is not None: + if machine is not None and not is_lite: arcade = await self.data.arcade.get_arcade(machine["arcade"]) if self.config.server.check_arcade_ip: if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip: @@ -257,7 +282,10 @@ class AllnetServlet: ) self.logger.warning(msg) - resp.stat = ALLNET_STAT.bad_game.value + if is_lite: + resp.result = ALLNET_STAT.bad_game.value + else: + resp.stat = ALLNET_STAT.bad_game.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") @@ -265,8 +293,12 @@ class AllnetServlet: self.logger.info( f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" ) - resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" - resp.host = f"{self.config.server.hostname}:{self.config.server.port}" + if is_lite: + resp.uri1 = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.uri2 = f"{self.config.server.hostname}:{self.config.server.port}" + else: + resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.server.hostname}:{self.config.server.port}" resp_dict = {k: v for k, v in vars(resp).items() if v is not None} resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) @@ -277,10 +309,16 @@ class AllnetServlet: int_ver = req.ver.replace(".", "") try: - resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) + if is_lite: + resp.uri1, resp.uri2 = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) + else: + resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) except Exception as e: self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}") - resp.stat = ALLNET_STAT.bad_game.value + if is_lite: + resp.result = ALLNET_STAT.bad_game.value + else: + resp.stat = ALLNET_STAT.bad_game.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") @@ -308,6 +346,9 @@ class AllnetServlet: "Pragma": "DFI", }, ) + elif is_lite: + iv = bytes([random.randint(2, 255) for _ in range(16)]) + return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp_str)) return PlainTextResponse(resp_str) @@ -517,6 +558,17 @@ class AllnetServlet: zipped = zlib.compress(unzipped) return base64.b64encode(zipped) + def dec_lite(self, key, iv, data): + cipher = AES.new(bytes(key), AES.MODE_CBC, iv) + decrypted = cipher.decrypt(data) + return decrypted[16:].decode("utf-8") + + def enc_lite(self, key, iv, data): + decrypted = pad(bytes([0] * 16) + data.encode('utf-8'), 16) + cipher = AES.new(bytes(key), AES.MODE_CBC, iv) + encrypted = cipher.encrypt(decrypted) + return encrypted + class BillingServlet: def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None: self.config = core_cfg @@ -773,6 +825,15 @@ class AllnetPowerOnResponse: self.minute = datetime.now().minute self.second = datetime.now().second +class AllnetPowerOnRequestLite: + def __init__(self, req: Dict) -> None: + if req is None: + raise AllnetRequestException("Request processing failed") + self.game_id: str = req.get("title_id", None) + self.ver: str = req.get("title_ver", None) + self.serial: str = req.get("client_id", None) + self.token: str = req.get("token", None) + class AllnetPowerOnResponse3(AllnetPowerOnResponse): def __init__(self, token) -> None: super().__init__() @@ -804,6 +865,30 @@ class AllnetPowerOnResponse2(AllnetPowerOnResponse): self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" +class AllnetPowerOnResponseLite: + def __init__(self, token) -> None: + # Custom Allnet Lite response + self.result = 1 + self.place_id = "0123" + self.uri1 = "" + self.uri2 = "" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" + self.setting = "1" + self.region0 = "1" + self.region_name0 = "W" + self.region_name1 = "" + self.region_name2 = "" + self.region_name3 = "" + self.country = "CHN" + self.location_type = "1" + self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + self.client_timezone = "+0800" + self.res_ver = "3" + self.token = token + class AllnetDownloadOrderRequest: def __init__(self, req: Dict) -> None: self.game_id = req.get("game_id", "") @@ -1068,6 +1153,7 @@ app_billing = Starlette( allnet = AllnetServlet(cfg, cfg_dir) route_lst = [ Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), + Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]), Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), diff --git a/core/app.py b/core/app.py index fa1c8f2..b450b0c 100644 --- a/core/app.py +++ b/core/app.py @@ -75,6 +75,7 @@ if not cfg.allnet.standalone: allnet = AllnetServlet(cfg, cfg_dir) route_lst += [ Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), + Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]), Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), diff --git a/core/config.py b/core/config.py index eb02c4e..7b6833d 100644 --- a/core/config.py +++ b/core/config.py @@ -378,6 +378,11 @@ class AllnetConfig: return CoreConfig.get_config_field( self.__config, "core", "allnet", "save_billing", default=False ) + @property + def allnet_lite_key(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "allnet_lite_key", default=[] + ) class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: diff --git a/example_config/core.yaml b/example_config/core.yaml index fa04a67..53063e3 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -46,6 +46,7 @@ allnet: allow_online_updates: False update_cfg_folder: "" save_billing: True + allnet_lite_key: [] billing: standalone: True From 1e4cb0b380ec1993dc0f95b7a3d6a01ddbbe484f Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Sat, 31 May 2025 08:44:51 +1200 Subject: [PATCH 16/63] Simple Download Order implementation for Allnet Lite --- core/allnet.py | 40 ++++++++++++++++++++++++++++++++++++++-- core/app.py | 1 + 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 2115b9a..96c5631 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -355,12 +355,20 @@ class AllnetServlet: async def handle_dlorder(self, request: Request): request_ip = Utils.get_ip_addr(request) pragma_header = request.headers.get('Pragma', "") + useragent_header = request.headers.get('User-Agent', "") is_dfi = pragma_header == "DFI" + is_lite = useragent_header[5:] == "Windows/Lite" data = await request.body() + if not self.config.allnet.allnet_lite_key and is_lite: + self.logger.error("!!!LITE KEY NOT SET!!!") + os._exit(1) + try: if is_dfi: req_urlencode = self.from_dfi(data) + elif is_lite: + req_urlencode = self.dec_lite(self.config.allnet.allnet_lite_key, data[:16], data) else: req_urlencode = data.decode() @@ -368,7 +376,10 @@ class AllnetServlet: if req_dict is None: raise AllnetRequestException() - req = AllnetDownloadOrderRequest(req_dict[0]) + if is_lite: + req = AllnetDownloadOrderRequestLite(req_dict[0]) + else: + req = AllnetDownloadOrderRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using if not req.game_id or not req.ver or not req.serial: @@ -384,7 +395,11 @@ class AllnetServlet: self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" ) - resp = AllnetDownloadOrderResponse(serial=req.serial) + + if is_lite: + resp = AllnetDownloadOrderResponseLite() + else: + resp = AllnetDownloadOrderResponse(serial=req.serial) if ( not self.config.allnet.allow_online_updates @@ -395,6 +410,9 @@ class AllnetServlet: return PlainTextResponse( self.to_dfi(resp) + b"\r\n", headers={ "Pragma": "DFI" } ) + elif is_lite: + iv = bytes([random.randint(2, 255) for _ in range(16)]) + return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp)) return PlainTextResponse(resp) else: @@ -405,6 +423,9 @@ class AllnetServlet: return PlainTextResponse( self.to_dfi(resp) + b"\r\n", headers={ "Pragma": "DFI" } ) + elif is_lite: + iv = bytes([random.randint(2, 255) for _ in range(16)]) + return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp)) return PlainTextResponse(resp) if path.exists( @@ -434,6 +455,9 @@ class AllnetServlet: "Pragma": "DFI", }, ) + elif is_lite: + iv = bytes([random.randint(2, 255) for _ in range(16)]) + return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, res_str)) return PlainTextResponse(res_str) @@ -896,12 +920,23 @@ class AllnetDownloadOrderRequest: self.serial = req.get("serial", "") self.encode = req.get("encode", "") +class AllnetDownloadOrderRequestLite: + def __init__(self, req: Dict) -> None: + self.game_id = req.get("title_id", "") + self.ver = req.get("title_ver", "") + self.serial = req.get("client_id", "") + class AllnetDownloadOrderResponse: def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: self.stat = stat self.serial = serial self.uri = uri +class AllnetDownloadOrderResponseLite: + def __init__(self, result: int = 1, uri: str = "null") -> None: + self.result = result + self.uri = uri + class TraceDataType(Enum): CHARGE = 0 EVENT = 1 @@ -1155,6 +1190,7 @@ route_lst = [ Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]), Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), + Route("/net/delivery/instruction", allnet.handle_dlorder, methods=["GET", "POST"]), Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), Route("/naomitest.html", allnet.handle_naomitest), diff --git a/core/app.py b/core/app.py index b450b0c..ecae1bf 100644 --- a/core/app.py +++ b/core/app.py @@ -77,6 +77,7 @@ if not cfg.allnet.standalone: Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]), Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), + Route("/net/delivery/instruction", allnet.handle_dlorder, methods=["GET", "POST"]), Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), Route("/naomitest.html", allnet.handle_naomitest), From 3e848d684fd7da6195234e6c7a2b95474b98a5bf Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Sat, 31 May 2025 09:36:53 +1200 Subject: [PATCH 17/63] SDHJ title server support added + encryption --- titles/chuni/__init__.py | 2 +- titles/chuni/const.py | 1 + titles/chuni/index.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 226594a..faacc0f 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -8,4 +8,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader frontend = ChuniFrontend -game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT] +game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT, ChuniConstants.GAME_CODE_CHN] diff --git a/titles/chuni/const.py b/titles/chuni/const.py index fd05003..7c534d3 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -6,6 +6,7 @@ class ChuniConstants: GAME_CODE = "SDBT" GAME_CODE_NEW = "SDHD" GAME_CODE_INT = "SDGS" + GAME_CODE_CHN = "SDHJ" CONFIG_NAME = "chuni.yaml" diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 080c041..7e72650 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -150,6 +150,11 @@ class ChuniServlet(BaseServlet): and version_idx >= ChuniConstants.VER_CHUNITHM_NEW ): method_fixed += "C3Exp" + elif ( + isinstance(version, str) + and version.endswith("_chn") + ): + method_fixed += "Chn" hash = PBKDF2( method_fixed, @@ -259,6 +264,13 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif version >= 135: # LUMINOUS PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS + elif game_code == "SDHJ": # Chn + if version < 110: # NEW!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif version >= 110 and version < 120: # NEW PLUS!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 120: # LUMINOUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS 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 @@ -268,6 +280,9 @@ class ChuniServlet(BaseServlet): if game_code == "SDGS": crypto_cfg_key = f"{internal_ver}_int" hash_table_key = f"{internal_ver}_int" + elif game_code == "SDHJ": + crypto_cfg_key = f"{internal_ver}_chn" + hash_table_key = f"{internal_ver}_chn" else: crypto_cfg_key = internal_ver hash_table_key = internal_ver @@ -337,6 +352,8 @@ class ChuniServlet(BaseServlet): endpoint = endpoint.replace("C3Exp", "") elif game_code == "SDGS" and version < 110: endpoint = endpoint.replace("Exp", "") + elif game_code == "SDHJ": + endpoint = endpoint.replace("Chn", "") else: endpoint = endpoint From 4875caab93fac30c4b7a33c9b71b28c94d0b406a Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Sat, 31 May 2025 12:23:40 +1200 Subject: [PATCH 18/63] ChimeDB qr code lookup/userid complete --- core/app.py | 9 +++ core/chimedb.py | 139 +++++++++++++++++++++++++++++++++++++++ core/config.py | 23 +++++++ example_config/core.yaml | 5 ++ 4 files changed, 176 insertions(+) create mode 100644 core/chimedb.py diff --git a/core/app.py b/core/app.py index ecae1bf..4737030 100644 --- a/core/app.py +++ b/core/app.py @@ -11,6 +11,7 @@ from typing import List from core import CoreConfig, TitleServlet, MuchaServlet from core.allnet import AllnetServlet, BillingServlet +from core.chimedb import ChimeServlet from core.frontend import FrontendServlet async def dummy_rt(request: Request): @@ -89,6 +90,14 @@ if not cfg.allnet.standalone: Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), ] +if cfg.chimedb.enable: + chimedb = ChimeServlet(cfg, cfg_dir) + route_lst += [ + Route("/wc_aime/api/alive_check", chimedb.handle_qr_alive, methods=["POST"]), + Route("/qrcode/api/alive_check", chimedb.handle_qr_alive, methods=["POST"]), + Route("/wc_aime/api/get_data", chimedb.handle_qr_lookup, methods=["POST"]) + ] + for code, game in title.title_registry.items(): route_lst += game.get_routes() diff --git a/core/chimedb.py b/core/chimedb.py new file mode 100644 index 0000000..6e87f69 --- /dev/null +++ b/core/chimedb.py @@ -0,0 +1,139 @@ +import hashlib +import json +import logging +from enum import Enum +from logging.handlers import TimedRotatingFileHandler + +import coloredlogs +from starlette.responses import PlainTextResponse +from starlette.requests import Request + +from core.config import CoreConfig +from core.data import Data + +class ChimeDBStatus(Enum): + NONE = 0 + READER_SETUP_FAIL = 1 + READER_ACCESS_FAIL = 2 + READER_INCOMPATIBLE = 3 + DB_RESOLVE_FAIL = 4 + DB_ACCESS_TIMEOUT = 5 + DB_ACCESS_FAIL = 6 + AIME_ID_INVALID = 7 + NO_BOARD_INFO = 8 + LOCK_BAN_SYSTEM_USER = 9 + LOCK_BAN_SYSTEM = 10 + LOCK_BAN_USER = 11 + LOCK_BAN = 12 + LOCK_SYSTEM_USER = 13 + LOCK_SYSTEM = 14 + LOCK_USER = 15 + +class ChimeServlet: + def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None: + self.config = core_cfg + self.config_folder = cfg_folder + + self.data = Data(core_cfg) + + self.logger = logging.getLogger("chimedb") + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Chimedb | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "chimedb"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.config.aimedb.loglevel) + coloredlogs.install( + level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True + + if not core_cfg.chimedb.key: + self.logger.error("!!!KEY NOT SET!!!") + exit(1) + + self.logger.info("Serving") + + async def handle_qr_alive(self, request: Request): + return PlainTextResponse("alive") + + async def handle_qr_lookup(self, request: Request) -> bytes: + req = json.loads(await request.body()) + access_code = req["qrCode"][-20:] + timestamp = req["timestamp"] + + try: + userId = await self._lookup(access_code) + data = json.dumps({ + "userID": userId, + "errorID": 0, + "timestamp": timestamp, + "key": self._hash_key(userId, timestamp) + }) + except Exception as e: + + self.logger.error(e.with_traceback(None)) + + data = json.dumps({ + "userID": -1, + "errorID": ChimeDBStatus.DB_ACCESS_FAIL, + "timestamp": timestamp, + "key": self._hash_key(-1, timestamp) + }) + + return PlainTextResponse(data) + + def _hash_key(self, chip_id, timestamp): + input_string = f"{chip_id}{timestamp}{self.config.chimedb.key}" + hash_object = hashlib.sha256(input_string.encode('utf-8')) + hex_dig = hash_object.hexdigest() + + formatted_hex = format(int(hex_dig, 16), '064x').upper() + + return formatted_hex + + async def _lookup(self, access_code): + user_id = await self.data.card.get_user_id_from_card(access_code) + + self.logger.info(f"access_code {access_code} -> user_id {user_id}") + + if not user_id or user_id <= 0: + user_id = await self._register(access_code) + + return user_id + + async def _register(self, access_code): + user_id = -1 + + if self.config.server.allow_user_registration: + user_id = await self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + else: + card_id = await self.data.card.create_card(user_id, access_code) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + self.logger.info( + f"Register access code {access_code} -> user_id {user_id}" + ) + else: + self.logger.info(f"Registration blocked!: access code {access_code}") + + return user_id diff --git a/core/config.py b/core/config.py index 7b6833d..2b45ed6 100644 --- a/core/config.py +++ b/core/config.py @@ -474,6 +474,28 @@ class AimedbConfig: self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400 ) +class ChimedbConfig: + def __init__(self, parent_config: "CoreConfig") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "chimedb", "enable", default=True + ) + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "chimedb", "loglevel", default="info" + ) + ) + @property + def key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "chimedb", "key", default="" + ) + class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -495,6 +517,7 @@ class CoreConfig(dict): self.allnet = AllnetConfig(self) self.billing = BillingConfig(self) self.aimedb = AimedbConfig(self) + self.chimedb = ChimedbConfig(self) self.mucha = MuchaConfig(self) @classmethod diff --git a/example_config/core.yaml b/example_config/core.yaml index 53063e3..5042c61 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -65,5 +65,10 @@ aimedb: id_secret: "" id_lifetime_seconds: 86400 +chimedb: + enable: False + loglevel: "info" + key: "" + mucha: loglevel: "info" From 33b7db0e985ecc5d04961d137a9de5120b9c6268 Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Sat, 31 May 2025 20:04:28 +1200 Subject: [PATCH 19/63] Added support for multiple Allnet Lite keys + extras --- core/allnet.py | 42 +++++++++++++++++++++++++++----------- core/config.py | 6 +++--- docs/config.md | 12 +++++++++++ docs/game_specific_info.md | 1 + titles/chuni/index.py | 3 +++ 5 files changed, 49 insertions(+), 15 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 96c5631..7cd9b43 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -139,17 +139,26 @@ class AllnetServlet: useragent_header = request.headers.get('User-Agent', "") is_dfi = pragma_header == "DFI" is_lite = useragent_header[5:] == "Windows/Lite" + lite_id = useragent_header[:4] data = await request.body() - if not self.config.allnet.allnet_lite_key and is_lite: - self.logger.error("!!!LITE KEY NOT SET!!!") - os._exit(1) + if not self.config.allnet.allnet_lite_keys and is_lite: + self.logger.error("!!!LITE KEYS NOT SET!!!") + raise AllnetRequestException() + elif is_lite: + for gameids, key in self.config.allnet.allnet_lite_keys.items(): + if gameids == lite_id: + litekey = key + + if is_lite and "litekey" not in locals(): + self.logger.error("!!!UNIQUE LITE KEY NOT FOUND!!!") + raise AllnetRequestException() try: if is_dfi: req_urlencode = self.from_dfi(data) elif is_lite: - req_urlencode = self.dec_lite(self.config.allnet.allnet_lite_key, data[:16], data) + req_urlencode = self.dec_lite(litekey, data[:16], data) else: req_urlencode = data @@ -348,7 +357,7 @@ class AllnetServlet: ) elif is_lite: iv = bytes([random.randint(2, 255) for _ in range(16)]) - return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp_str)) + return PlainTextResponse(content=self.enc_lite(litekey, iv, resp_str)) return PlainTextResponse(resp_str) @@ -358,17 +367,26 @@ class AllnetServlet: useragent_header = request.headers.get('User-Agent', "") is_dfi = pragma_header == "DFI" is_lite = useragent_header[5:] == "Windows/Lite" + lite_id = useragent_header[:4] data = await request.body() - if not self.config.allnet.allnet_lite_key and is_lite: - self.logger.error("!!!LITE KEY NOT SET!!!") - os._exit(1) + if not self.config.allnet.allnet_lite_keys and is_lite: + self.logger.error("!!!LITE KEYS NOT SET!!!") + raise AllnetRequestException() + elif is_lite: + for gameids, key in self.config.allnet.allnet_lite_keys.items(): + if gameids == lite_id: + litekey = key + + if is_lite and "litekey" not in locals(): + self.logger.error("!!!UNIQUE LITE KEY NOT FOUND!!!") + raise AllnetRequestException() try: if is_dfi: req_urlencode = self.from_dfi(data) elif is_lite: - req_urlencode = self.dec_lite(self.config.allnet.allnet_lite_key, data[:16], data) + req_urlencode = self.dec_lite(litekey, data[:16], data) else: req_urlencode = data.decode() @@ -412,7 +430,7 @@ class AllnetServlet: ) elif is_lite: iv = bytes([random.randint(2, 255) for _ in range(16)]) - return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp)) + return PlainTextResponse(content=self.enc_lite(litekey, iv, resp)) return PlainTextResponse(resp) else: @@ -425,7 +443,7 @@ class AllnetServlet: ) elif is_lite: iv = bytes([random.randint(2, 255) for _ in range(16)]) - return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, resp)) + return PlainTextResponse(content=self.enc_lite(litekey, iv, resp)) return PlainTextResponse(resp) if path.exists( @@ -457,7 +475,7 @@ class AllnetServlet: ) elif is_lite: iv = bytes([random.randint(2, 255) for _ in range(16)]) - return PlainTextResponse(content=self.enc_lite(self.config.allnet.allnet_lite_key, iv, res_str)) + return PlainTextResponse(content=self.enc_lite(litekey, iv, res_str)) return PlainTextResponse(res_str) diff --git a/core/config.py b/core/config.py index 2b45ed6..f79d6c0 100644 --- a/core/config.py +++ b/core/config.py @@ -1,7 +1,7 @@ import logging import os import ssl -from typing import Any, Union +from typing import Any, Union, Dict from typing_extensions import Optional @@ -379,9 +379,9 @@ class AllnetConfig: self.__config, "core", "allnet", "save_billing", default=False ) @property - def allnet_lite_key(self) -> bool: + def allnet_lite_keys(self) -> Dict: return CoreConfig.get_config_field( - self.__config, "core", "allnet", "allnet_lite_key", default=[] + self.__config, "core", "allnet", "allnet_lite_keys", default={} ) class BillingConfig: diff --git a/docs/config.md b/docs/config.md index f85e8e7..6cf2482 100644 --- a/docs/config.md +++ b/docs/config.md @@ -41,6 +41,13 @@ - `loglevel`: Logging level for the allnet server. Default `info` - `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False` - `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""` +- `allnet_lite_keys:` Allnet Lite (Chinese Allnet) PowerOn/DownloadOrder unique keys. Default ` ` +```yaml + allnet_lite_keys: + "SDJJ": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + "SDHJ": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + "SDGB": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] +``` ## Billing - `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False` - `loglevel`: Logging level for the billing server. Default `info` @@ -56,3 +63,8 @@ - `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` - `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""` - `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day) +## Chimedb +- `enable`: Whether or not chimedb should run. Default `False` +- `loglevel`: Logging level for the chimedb server. Default `info` +- `key`: Key to hash chimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle chimedb requests. Default `""` + diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 7121478..0903979 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -108,6 +108,7 @@ crypto: keys: 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] "13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42] + "13_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 8] ``` ### Database upgrade diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 7e72650..ad51bb0 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -101,14 +101,17 @@ class ChuniServlet(BaseServlet): f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS ChuniConstants.VER_CHUNITHM_NEW: 54, f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, + f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37, ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, + f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, ChuniConstants.VER_CHUNITHM_SUN: 70, f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, f"{ChuniConstants.VER_CHUNITHM_SUN_PLUS}_int": 36, ChuniConstants.VER_CHUNITHM_LUMINOUS: 8, f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8, + f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8, ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56, } From 02bfc7dba250e4d85bc7d0bbbce6aaa8b068c1b7 Mon Sep 17 00:00:00 2001 From: Keeboy99 <67620144+Keeboy99@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:31:57 +1200 Subject: [PATCH 20/63] SDGB support + extras --- core/allnet.py | 4 ++-- readme.md | 29 +++++++++++++++++++++++++++++ titles/chuni/index.py | 6 +++--- titles/mai2/__init__.py | 1 + titles/mai2/base.py | 3 +++ titles/mai2/const.py | 1 + titles/mai2/index.py | 16 ++++++++++++++++ 7 files changed, 55 insertions(+), 5 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 7cd9b43..94b34ab 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -606,9 +606,9 @@ class AllnetServlet: return decrypted[16:].decode("utf-8") def enc_lite(self, key, iv, data): - decrypted = pad(bytes([0] * 16) + data.encode('utf-8'), 16) + unencrypted = pad(bytes([0] * 16) + data.encode('utf-8'), 16) cipher = AES.new(bytes(key), AES.MODE_CBC, iv) - encrypted = cipher.encrypt(decrypted) + encrypted = cipher.encrypt(unencrypted) return encrypted class BillingServlet: diff --git a/readme.md b/readme.md index e29784d..8591c74 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,11 @@ Games listed below have been tested and confirmed working. Only game versions ol + 1.30 + 1.35 ++ CHUNITHM CHINA + + NEW + + 2024 (NEW) + + 2024 (LUMINOUS) + + CHUNITHM INTL + SUPERSTAR + SUPERSTAR PLUS @@ -15,6 +20,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + NEW PLUS + SUN + SUN PLUS + + LUMINOUS + + LUMINOUS PLUS + CHUNITHM JP + AIR @@ -43,7 +50,29 @@ Games listed below have been tested and confirmed working. Only game versions ol + Initial D THE ARCADE + Season 2 ++ maimai DX CHINA + + DX (Muji) + + 2021 (Muji) + + 2022 (Muji) + + 2023 (FESTiVAL) + + 2024 (BUDDiES) + ++ maimai DX INTL + + DX + + DX Plus + + Splash + + Splash Plus + + UNiVERSE + + UNiVERSE PLUS + + FESTiVAL + + FESTiVAL PLUS + + BUDDiES + + BUDDiES PLUS + + PRiSM + + maimai DX + + DX + + DX Plus + Splash + Splash Plus + UNiVERSE diff --git a/titles/chuni/index.py b/titles/chuni/index.py index ad51bb0..1392588 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -104,7 +104,7 @@ class ChuniServlet(BaseServlet): f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37, ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, - f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, + f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW ChuniConstants.VER_CHUNITHM_SUN: 70, f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, @@ -268,9 +268,9 @@ class ChuniServlet(BaseServlet): elif version >= 135: # LUMINOUS PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS elif game_code == "SDHJ": # Chn - if version < 110: # NEW!! + if version < 110: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 110 and version < 120: # NEW PLUS!! + elif 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 internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 234e864..4c1739d 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -18,4 +18,5 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, Mai2Constants.GAME_CODE_DX_INT, + Mai2Constants.GAME_CODE_DX_CHN, ] diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5d1c767..1983ef7 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -139,6 +139,9 @@ class Mai2Base: async def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} + async def handle_get_game_ng_word_list_api_request(self, data: Dict) -> Dict: + return {"ngWordExactMatchLength": 0, "ngWordExactMatchList": [], "ngWordPartialMatchLength": 0, "ngWordPartialMatchList": []} + async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = await self.data.static.get_enabled_tickets(self.version, 1) if game_charge_list is None: diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 99642b2..47e5cbd 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -32,6 +32,7 @@ class Mai2Constants: GAME_CODE_FINALE = "SDEY" GAME_CODE_DX = "SDEZ" GAME_CODE_DX_INT = "SDGA" + GAME_CODE_DX_CHN = "SDGB" CONFIG_NAME = "mai2.yaml" diff --git a/titles/mai2/index.py b/titles/mai2/index.py index d8e2a4f..d59ce87 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -337,6 +337,20 @@ class Mai2Servlet(BaseServlet): elif version >=150: internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + elif game_code == "SDGB": # Chn + if version < 110: # Muji + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 110 and version < 120: # Muji + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 120 and version < 130: # Muji (LMAO) + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 130 and version < 140: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 140 and version < 150: # BUDDiES + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + elif version >=150: + internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM + 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 # dealing with an encrypted request. False positives shouldn't happen @@ -403,6 +417,8 @@ class Mai2Servlet(BaseServlet): endpoint = ( endpoint.replace("MaimaiExp", "") if game_code == Mai2Constants.GAME_CODE_DX_INT + else endpoint.replace("MaimaiChn", "") + if game_code == Mai2Constants.GAME_CODE_DX_CHN else endpoint ) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" From faafee3cbe6e8404e51f6b29a53c3fbf73a5a579 Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sat, 14 Jun 2025 05:18:11 +0800 Subject: [PATCH 21/63] docker: add nginx in docker compose file --- docker-compose.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6a35355..51f4296 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: volumes: - ./aime:/app/aime - ./configs/config:/app/config + - ./cert:/app/cert environment: CFG_DEV: 1 @@ -14,7 +15,8 @@ services: CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached CFG_CORE_AIMEDB_KEY: CFG_CHUNI_SERVER_LOGLEVEL: debug - + + ##Note: comment 80 and 8443 when you plan to use with nginx ports: - "80:80" - "8443:8443" @@ -64,3 +66,18 @@ services: ports: - "9090:8080" + ##Note: uncomment to allow use nginx with artemis, don't forget to comment 80 and 8443 ports on artemis + # nginx: + # hostname: ma.nginx + # image: nginx:latest + # ports: + # - "80:80" + # - "443:443" + # - "8443:8443" + # volumes: + ##Note: copy example_config/example_nginx.conf to configs/nginx folder, edit it and rename to nginx.conf + # - ./configs/nginx:/etc/nginx/conf.d + # - ./cert:/etc/nginx/cert + # - ./logs/nginx:/var/log/nginx + # depends_on: + # - app \ No newline at end of file From d8bd26d16ea647a3413302263ff0b2fc192a5e41 Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sat, 14 Jun 2025 05:29:25 +0800 Subject: [PATCH 22/63] nginx: add WAHLAP billing in example config file --- example_config/nginx_example.conf | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index b01a822..5823ba4 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -66,6 +66,52 @@ server { } } +# WAHLAP Billing, they use 443 port +# comment this out if running billing standalone +# still not work for some reason, please set +# billing=127.0.0.1 in segatools.ini for now and looking for fix +server { + listen 443 ssl; + server_name bl.sys-all.cn; + + ssl_certificate /path/to/cert/server.pem; + ssl_certificate_key /path/to/cert/server.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=0"; + ssl_prefer_server_ciphers off; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass http://127.0.0.1:8080/; + } +} + +server { + listen 443 ssl; + server_name bl.sys-allnet.cn; + + ssl_certificate /path/to/cert/server.pem; + ssl_certificate_key /path/to/cert/server.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=0"; + ssl_prefer_server_ciphers off; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass http://127.0.0.1:8080/; + } +} + # Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend server { listen 80; From 4a417f869aa4c249eb7b09a076cedb795874727c Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sat, 14 Jun 2025 05:33:37 +0800 Subject: [PATCH 23/63] docker: a little change --- docker-compose.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51f4296..beab3a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,17 +67,17 @@ services: - "9090:8080" ##Note: uncomment to allow use nginx with artemis, don't forget to comment 80 and 8443 ports on artemis - # nginx: - # hostname: ma.nginx - # image: nginx:latest - # ports: - # - "80:80" - # - "443:443" - # - "8443:8443" - # volumes: + #nginx: + # hostname: ma.nginx + # image: nginx:latest + # ports: + # - "80:80" + # - "443:443" + # - "8443:8443" + # volumes: ##Note: copy example_config/example_nginx.conf to configs/nginx folder, edit it and rename to nginx.conf - # - ./configs/nginx:/etc/nginx/conf.d - # - ./cert:/etc/nginx/cert - # - ./logs/nginx:/var/log/nginx - # depends_on: - # - app \ No newline at end of file + # - ./configs/nginx:/etc/nginx/conf.d + # - ./cert:/etc/nginx/cert + # - ./logs/nginx:/var/log/nginx + # depends_on: + # - app \ No newline at end of file From 592e6961e8c36e793fdbf189b7072ffa880df54e Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sat, 14 Jun 2025 05:05:19 +0800 Subject: [PATCH 24/63] chu&mai2: add option to use https --- example_config/chuni.yaml | 1 + example_config/mai2.yaml | 1 + titles/chuni/config.py | 6 ++++++ titles/chuni/index.py | 22 +++++++++++++++++++--- titles/mai2/config.py | 6 ++++++ titles/mai2/index.py | 27 +++++++++++++++++++++------ 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index a3781d6..e2ae746 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,6 +2,7 @@ server: enable: True loglevel: "info" news_msg: "" + use_https: False # for CRYSTAL PLUS and later or SUPERSTAR and later team: name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index f0d7754..52dd4be 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + use_https: False # for DX and later deliver: enable: False diff --git a/titles/chuni/config.py b/titles/chuni/config.py index f0e15f3..95720bc 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -25,6 +25,12 @@ class ChuniServerConfig: return CoreConfig.get_config_field( self.__config, "chuni", "server", "news_msg", default="" ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "server", "use_https", default=False + ) class ChuniTeamConfig: diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 1392588..cc25289 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -190,10 +190,26 @@ class ChuniServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) - return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) + if self.game_cfg.server.use_https and ( + (game_code == "SDBT" and game_ver >= 145) or # JP use TLS from CRYSTAL PLUS + game_code != "SDBT" # SDGS and SDHJ all version can use TLS + ): + proto = "https" + else: + proto = "http" + + if proto == "https": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" + else: + t_port = f":{title_port_int}" if title_port_int != 80 else "" + + return ( + f"{proto}://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}", + ) def get_routes(self) -> List[Route]: return [ diff --git a/titles/mai2/config.py b/titles/mai2/config.py index efd3ba5..66c1c94 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -20,6 +20,12 @@ class Mai2ServerConfig: self.__config, "mai2", "server", "loglevel", default="info" ) ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "use_https", default=False + ) class Mai2DeliverConfig: def __init__(self, parent: "Mai2Config") -> None: diff --git a/titles/mai2/index.py b/titles/mai2/index.py index d59ce87..e9d78d9 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -157,14 +157,29 @@ class Mai2Servlet(BaseServlet): ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return ( - f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - f"{self.core_cfg.server.hostname}", - ) + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) + + if self.game_cfg.server.use_https: + if (game_code == "SDEZ" and game_ver >= 114) or (game_code == "SDGA" and game_ver >= 110): # SDEZ and SDGA use tls from Splash version + proto = "" # game will auto add https:// in uri with original code + elif game_code == "SDGB" and game_ver >= 130: # SDGB use tls from 1.30 + # game will check if uri start with "http:", if yes, set IsHttpConnection = true + # so we can return https://example.com or http://example.com, all will work + proto = "https://" + else: + # "maimai", SDEZ 1.00 ~ 1.13, SDGA 1.00 ~ 1.06 and SDGB 1.01, 1.20 use http:// + proto = "http://" + else: + proto = "http://" + + if proto == "" or proto == "https://": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" + else: + t_port = f":{title_port_int}" if title_port_int != 80 else "" return ( - f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", + f"{proto}{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", f"{self.core_cfg.server.hostname}", ) From cf6666b866a8ccc070e85bea084f03a6fccc346f Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sun, 8 Jun 2025 19:10:25 +0000 Subject: [PATCH 25/63] mai2: fix Exp and Chn encryption --- docs/game_specific_info.md | 25 +++++++++++++ example_config/core.yaml | 2 +- titles/mai2/index.py | 74 ++++++++++++++++++++++++++++---------- 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 0903979..e05a771 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -258,6 +258,31 @@ python dbutils.py upgrade Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! +### Config + +Config file is located in `config/mai2.yaml`. + +| Option | Info | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| `crypto` | This option is used to enable the TLS Encryption | + + +If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key +is the version ID for Japanese (SDEZ) versions and `"{versionID}_int"` for Export (SDGA) versions, and the value +is an array containing `[key, iv, salt]` in order. + +Just copy your salt in here, no need to convert anything. + + +```yaml +crypto: + encrypted_only: False + keys: + 23: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "23_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] +``` + ## Hatsune Miku Project Diva ### SBZV diff --git a/example_config/core.yaml b/example_config/core.yaml index 5042c61..613883f 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -46,7 +46,7 @@ allnet: allow_online_updates: False update_cfg_folder: "" save_billing: True - allnet_lite_key: [] + allnet_lite_keys: [] billing: standalone: True diff --git a/titles/mai2/index.py b/titles/mai2/index.py index d59ce87..807422a 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -38,7 +38,7 @@ class Mai2Servlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = Mai2Config() - self.hash_table: Dict[int, Dict[str, str]] = {} + self.hash_table: Dict[str, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) @@ -97,16 +97,21 @@ class Mai2Servlet(BaseServlet): self.logger.initted = True for version, keys in self.game_cfg.crypto.keys.items(): - if version < Mai2Constants.VER_MAIMAI_DX: + if int(str(version).split('_')[0]) < Mai2Constants.VER_MAIMAI_DX: continue if len(keys) < 3: continue + if isinstance(version, int): + version_idx = version + else: + version_idx = int(version.split("_")[0]) + self.hash_table[version] = {} method_list = [ method - for method in dir(self.versions[version]) + for method in dir(self.versions[version_idx]) if not method.startswith("__") ] @@ -115,6 +120,21 @@ class Mai2Servlet(BaseServlet): # remove the first 6 chars and the final 7 chars to get the canonical # endpoint name. method_fixed = inflection.camelize(method)[6:-7] + + # This only applies for maimai DX International and later for some reason. + if ( + isinstance(version, str) + and version.endswith("_int") + and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE + ): + method_fixed += "MaimaiExp" + elif ( + isinstance(version, str) + and version.endswith("_chn") + and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE # 1.00, 1.11 and 1.20 all use DX, but they add MaimaiChn in 1.20, we set 1.20 to use UNIVERSE code + ): + method_fixed += "MaimaiChn" + hash = MD5.new((method_fixed + keys[2]).encode()) # truncate unused bytes like the game does @@ -310,7 +330,7 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES elif version >= 145 and version <150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM elif game_code == "SDGA": # Int @@ -334,47 +354,58 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES elif version >= 145 and version <150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM elif game_code == "SDGB": # Chn if version < 110: # Muji internal_ver = Mai2Constants.VER_MAIMAI_DX elif version >= 110 and version < 120: # Muji - internal_ver = Mai2Constants.VER_MAIMAI_DX + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH # still DX, but need Splash to set encryption key elif version >= 120 and version < 130: # Muji (LMAO) - internal_ver = Mai2Constants.VER_MAIMAI_DX + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE # still DX, but need UNIVERSE to set encryption key elif version >= 130 and version < 140: # FESTiVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL elif version >= 140 and version < 150: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: + + if game_code == "SDGA": + crypto_cfg_key = f"{internal_ver}_int" + hash_table_key = f"{internal_ver}_int" + elif game_code == "SDGB": + crypto_cfg_key = f"{internal_ver}_chn" + hash_table_key = f"{internal_ver}_chn" + else: + crypto_cfg_key = internal_ver + hash_table_key = internal_ver + # If we get a 32 character long hex string, it's a hash and we're # dealing with an encrypted request. False positives shouldn't happen # as long as requests are suffixed with `Api`. - if internal_ver not in self.hash_table: + if hash_table_key not in self.hash_table: self.logger.error( "v%s does not support encryption or no keys entered", version, ) return Response(zlib.compress(b'{"stat": "0"}')) - elif endpoint.lower() not in self.hash_table[internal_ver]: + elif endpoint.lower() not in self.hash_table[hash_table_key]: self.logger.error( "No hash found for v%s endpoint %s", version, endpoint ) return Response(zlib.compress(b'{"stat": "0"}')) - endpoint = self.hash_table[internal_ver][endpoint.lower()] + endpoint = self.hash_table[hash_table_key][endpoint.lower()] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) req_raw = crypt.decrypt(req_raw) @@ -392,7 +423,10 @@ class Mai2Servlet(BaseServlet): if ( not encrypted and self.game_cfg.crypto.encrypted_only - and version >= 110 + and ( + # SDEZ start from 1.10, SDGA and SDGB keep use encryption from 1.00 + internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or game_code != "SDEZ" + ) ): self.logger.error( "Unencrypted v%s %s request, but config is set to encrypted only: %r", @@ -416,9 +450,9 @@ class Mai2Servlet(BaseServlet): endpoint = ( endpoint.replace("MaimaiExp", "") - if game_code == Mai2Constants.GAME_CODE_DX_INT + if game_code == Mai2Constants.GAME_CODE_DX_INT and version >= 120 else endpoint.replace("MaimaiChn", "") - if game_code == Mai2Constants.GAME_CODE_DX_CHN + if game_code == Mai2Constants.GAME_CODE_DX_CHN and version >= 120 else endpoint ) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -444,15 +478,17 @@ class Mai2Servlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrypted or version < 110: + if not encrypted or ( + internal_ver < Mai2Constants.VER_MAIMAI_DX_PLUS and game_code == "SDEZ" + ): return Response(zipped) padded = pad(zipped, 16) crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) return Response(crypt.encrypt(padded)) From 6e5a83f3d7fa256f30b6c1a542ba4731ecef7c1e Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sat, 14 Jun 2025 12:24:07 +0000 Subject: [PATCH 26/63] mai2: correct game code check when not encrypted and enable encrypted only --- titles/mai2/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 807422a..6d7bb5d 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -425,7 +425,7 @@ class Mai2Servlet(BaseServlet): and self.game_cfg.crypto.encrypted_only and ( # SDEZ start from 1.10, SDGA and SDGB keep use encryption from 1.00 - internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or game_code != "SDEZ" + internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or (game_code == "SDGA" or game_code == "SDGB") ) ): self.logger.error( From 0a4dc8dbb0e4530618ea0a89df58a00d35cb0f26 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Tue, 22 Jul 2025 19:55:33 +0800 Subject: [PATCH 27/63] add Gate 7,8,9,10 support --- titles/mai2/const.py | 10 +++++++++- titles/mai2/prismplus.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 1813ca6..e395ba0 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -101,8 +101,16 @@ class Mai2Constants: #黑の扉: Played 11 songs 5: [11003, 11095, 11152, 11224, 11296, 11375, 11452, 11529, 11608, 11669, 11736, 11806], #黄の扉: Use random selection to play one of the songs - 6: [212, 213, 337, 270, 271, 11504, 339, 453, 11336, 11852] + 6: [212, 213, 337, 270, 271, 11504, 339, 453, 11336, 11852], #赤の扉: Played 10 songs + 7: [], + #PRISM TOWER: Get the key after clearing six doors. + 8: [], + #KALEIDXSCOPE_FIRST_STAGE: Clear Prism Tower + 9: [], + #希望の扉: CLEAR KALEIDXSCOPE_FIRST_STAGE + 10: [] + #KALEIDXSCOPE_SECOND_STAGE: JP: scan the DXPASS of 希望の鍵, will automatically unlock after clearing 希望の扉 in artemis } MAI_VERSION_LUT = { "100": VER_MAIMAI, diff --git a/titles/mai2/prismplus.py b/titles/mai2/prismplus.py index 1719189..04b8bc4 100644 --- a/titles/mai2/prismplus.py +++ b/titles/mai2/prismplus.py @@ -33,14 +33,17 @@ class Mai2PrismPlus(Mai2Prism): {"gateId": 4, "phaseId": 6}, {"gateId": 5, "phaseId": 6}, {"gateId": 6, "phaseId": 6}, - {"gateId": 7, "phaseId": 6} + {"gateId": 7, "phaseId": 6}, + {"gateId": 8, "phaseId": 6}, + {"gateId": 9, "phaseId": 6}, + {"gateId": 10, "phaseId": 13} ] } async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: # kaleidxscope keyget condition judgement # player may get key before GateFound - for gate in range(1,8): + for gate in range(1,11): if gate == 1 or gate == 4 or gate == 6: condition_satisfy = 0 for condition in Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]: @@ -95,6 +98,32 @@ class Mai2PrismPlus(Mai2Prism): new_kaleidxscope = {'gateId': 5, "isKeyFound": True} await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + elif gate == 7: + + played_kaleidxscope_list = await self.data.score.get_user_kaleidxscope_list(data["userId"]) + check_results = {} + for i in range(1,7): + check_results[i] = False + for played_kaleidxscope in played_kaleidxscope_list: + if played_kaleidxscope[2] == i and played_kaleidxscope[5] == True: + check_results[i] = True + break + all_true = all(check_results.values()) + + if all_true: + new_kaleidxscope = {'gateId': 7, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + elif gate == 10: + + played_kaleidxscope_list = await self.data.score.get_user_kaleidxscope_list(data["userId"]) + for played_kaleidxscope in played_kaleidxscope_list: + if played_kaleidxscope[2] == 9 and played_kaleidxscope[5] == True: + new_kaleidxscope = {'gateId': 10, "isGateFound": True, "isKeyFound": True} + await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope) + + + kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"]) if kaleidxscope is None: From 18701576e32a16bf72d2b8bc98bf989682ea2f56 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 24 Jul 2025 20:20:08 +0800 Subject: [PATCH 28/63] add support for GetGameMusicScoreApi the api is used to deliver charts from the server for mai2 Prism and later --- example_config/mai2.yaml | 4 ++++ titles/mai2/config.py | 19 ++++++++++++++++- titles/mai2/prism.py | 46 ++++++++++++++++++++++++++++++++-------- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index 52dd4be..094d608 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -8,6 +8,10 @@ deliver: udbdl_enable: False content_folder: "" +chart_deliver: #for Prism and later + enable: False + chart_folder: "" + uploads: photos: False photos_dir: "" diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 66c1c94..2e24bc9 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -77,6 +77,22 @@ class Mai2UploadsConfig: self.__config, "mai2", "uploads", "movies_dir", default="" ) +class Mai2OnlineChartsConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "chart_deliver", "enable", default=False + ) + + @property + def chart_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "chart_deliver", "chart_folder", default="" + ) + class Mai2CryptoConfig: def __init__(self, parent_config: "Mai2Config") -> None: @@ -106,4 +122,5 @@ class Mai2Config(dict): self.server = Mai2ServerConfig(self) self.deliver = Mai2DeliverConfig(self) self.uploads = Mai2UploadsConfig(self) - self.crypto = Mai2CryptoConfig(self) \ No newline at end of file + self.crypto = Mai2CryptoConfig(self) + self.charts = Mai2OnlineChartsConfig(self) \ No newline at end of file diff --git a/titles/mai2/prism.py b/titles/mai2/prism.py index 95ebb74..d5aae58 100644 --- a/titles/mai2/prism.py +++ b/titles/mai2/prism.py @@ -1,3 +1,5 @@ +import base64 +import os from typing import Dict from core.config import CoreConfig @@ -25,16 +27,42 @@ class Mai2Prism(Mai2BuddiesPlus): "userItemList": [] } - #seems to be used for downloading music scores online + #used for downloading music scores online async def handle_get_game_music_score_api_request(self, data: Dict) -> Dict: - return { - "gameMusicScore": { - "musicId": data["musicId"], - "level": data["level"], - "type": data["type"], - "scoreData": "" - } - } + if not self.game_config.charts.enable or not self.game_config.charts.chart_folder: + return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}} + + padded_music_id = str(data["musicId"]).zfill(6) + padded_level_id = str(data["level"]).zfill(2) + + if data["type"] == 0: + target_filename = f"{padded_music_id}_{padded_level_id}.ma2" + elif data["type"] == 1: + target_filename = f"{padded_music_id}_{padded_level_id}_L.ma2" + elif data["type"] == 2: + target_filename = f"{padded_music_id}_{padded_level_id}_R.ma2" + else: + self.logger.error("Valid MusicScore type!") + return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}} + + + chart_path = os.path.join(self.game_config.charts.chart_folder, target_filename) + if os.path.isfile(chart_path): + with open(chart_path, 'rb') as file: + file_content = file.read() + base64_content = base64.b64encode(file_content).decode('ascii') + return { + "gameMusicScore": { + "musicId": data["musicId"], + "level": data["level"], + "type": data["type"], + "scoreData": base64_content + } + } + else: + self.logger.warning(f"Chart {target_filename} not found!") + return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}} + async def handle_get_game_kaleidx_scope_api_request(self, data: Dict) -> Dict: return { From 2a12f84dd9ad0691fb39a2509e6c41c548cd177c Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Sat, 26 Jul 2025 18:42:48 +0800 Subject: [PATCH 29/63] add implement of GetUserFriendCheckApi and UserFriendRegistApi --- titles/mai2/buddiesplus.py | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/titles/mai2/buddiesplus.py b/titles/mai2/buddiesplus.py index e87fae6..fc0bcfb 100644 --- a/titles/mai2/buddiesplus.py +++ b/titles/mai2/buddiesplus.py @@ -58,3 +58,70 @@ class Mai2BuddiesPlus(Mai2Buddies): "friendBonusFlag": False } } + + async def handle_get_user_friend_check_api_request(self, data: Dict) -> Dict: + user1rivalList = await self.data.profile.get_rivals(data["userId1"]) + user2rivalList = await self.data.profile.get_rivals(data["userId2"]) + + is_user2_in_user1_rivals = any(rival["rival"] == data["userId2"] for rival in user1rivalList) + is_user1_in_user2_rivals = any(rival["rival"] == data["userId1"] for rival in user2rivalList) + + if is_user2_in_user1_rivals and is_user1_in_user2_rivals: + return {"returnCode": 0} + else: + return {"returnCode": 1} + + async def handle_user_friend_regist_api_request(self, data: Dict) -> Dict: + user1rivalList = await self.data.profile.get_rivals(data["userId1"]) or [] + user2rivalList = await self.data.profile.get_rivals(data["userId2"]) or [] + + is_user2_in_user1_rivals = any(rival["rival"] == data["userId2"] for rival in user1rivalList) + is_user1_in_user2_rivals = any(rival["rival"] == data["userId1"] for rival in user2rivalList) + user1_show_count = sum(1 for rival in user1rivalList if rival.get("show") == 1) + user2_show_count = sum(1 for rival in user2rivalList if rival.get("show") == 1) + + # initialize returnCode + returnCode1 = 2 + returnCode2 = 2 + + # Case1 no rival + if not is_user2_in_user1_rivals and not is_user1_in_user2_rivals: + if user1_show_count >= 3 and user2_show_count >= 3: + returnCode1, returnCode2 = 1, 1 + elif user1_show_count >= 3: + returnCode1, returnCode2 = 1, 2 + elif user2_show_count >= 3: + returnCode1, returnCode2 = 2, 1 + + # Case2 has single rival + elif is_user2_in_user1_rivals != is_user1_in_user2_rivals: + if is_user2_in_user1_rivals and not is_user1_in_user2_rivals: + if user1_show_count >= 3 and user2_show_count >= 3: + returnCode1, returnCode2 = 1, 1 + elif user1_show_count >= 3: + returnCode1, returnCode2 = 1, 2 + elif user2_show_count >= 3: + returnCode1, returnCode2 = 2, 1 + else: + if user1_show_count >= 3 and user2_show_count >= 3: + returnCode1, returnCode2 = 1, 1 + elif user1_show_count >= 3: + returnCode1, returnCode2 = 1, 2 + elif user2_show_count >= 3: + returnCode1, returnCode2 = 2, 1 + + # execute add_rival and show_rival + if not is_user2_in_user1_rivals: + await self.data.profile.add_rival(data["userId1"], data["userId2"]) + if returnCode1 == 2 and user1_show_count < 3: + await self.data.profile.set_rival_shown(data["userId1"], data["userId2"], True) + + if not is_user1_in_user2_rivals: + await self.data.profile.add_rival(data["userId2"], data["userId1"]) + if returnCode2 == 2 and user2_show_count < 3: + await self.data.profile.set_rival_shown(data["userId2"], data["userId1"], True) + + return { + "returnCode1": returnCode1, + "returnCode2": returnCode2 + } \ No newline at end of file From 1833e8abdeba8feaad9eaf9483ab622c316c9e2d Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Sun, 27 Jul 2025 03:06:30 +0800 Subject: [PATCH 30/63] add mai2 rival management features to frontend and templates --- titles/mai2/frontend.py | 42 ++++++++- titles/mai2/templates/mai2_index.jinja | 119 +++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py index c760e13..aa030b8 100644 --- a/titles/mai2/frontend.py +++ b/titles/mai2/frontend.py @@ -46,6 +46,9 @@ class Mai2Frontend(FE_Base): Route("/update.name", self.update_name, methods=['POST']), Route("/version.change", self.version_change, methods=['POST']), Route("/photo/{photo_id}", self.get_photo, methods=['GET']), + Route("/rival.add", self.rival_POST, methods=['POST']), + Route("/rival.delete", self.rival_POST, methods=['POST']), + Route("/rival.show", self.rival_POST, methods=['POST']), ] async def render_GET(self, request: Request) -> bytes: @@ -61,11 +64,22 @@ class Mai2Frontend(FE_Base): if usr_sesh.user_id > 0: versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) profile = [] + new_rival_list = [] if versions: # maimai_version is -1 means it is not initialized yet, select a default version from existing. if incoming_ver < 0: usr_sesh.maimai_version = versions[0]['version'] profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) + rival_list = await self.data.profile.get_rivals(usr_sesh.user_id) + + for rival in rival_list: + rivalid = rival["rival"] + rivalShow = rival["show"] + rivalprofile = await self.data.profile.get_profile_detail(rivalid, usr_sesh.maimai_version) + rivalName = rivalprofile["userName"] if rivalprofile else "UnknownName" + rivalRating = rivalprofile["playerRating"] if rivalprofile else 0 + new_rival = (rivalName, rivalRating, rivalid, rivalShow) + new_rival_list.append(new_rival) versions = [x['version'] for x in versions] resp = Response(template.render( @@ -76,7 +90,8 @@ class Mai2Frontend(FE_Base): profile=profile, version_list=Mai2Constants.VERSION_STRING, versions=versions, - cur_version=usr_sesh.maimai_version + cur_version=usr_sesh.maimai_version, + rival_list=new_rival_list ), media_type="text/html; charset=utf-8") if incoming_ver < 0: @@ -420,3 +435,28 @@ class Mai2Frontend(FE_Base): return FileResponse(f"{out_folder}.jpeg") return Response(status_code=404) + async def rival_POST(self, request: Request): + uri = request.url.path + frm = await request.form() + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if uri == "/game/mai2/rival.add": + rival_id = frm.get("rivalUserId") + await self.data.profile.add_rival(usr_sesh.user_id, rival_id) + # self.logger.info(f"{usr_sesh.user_id} added a rival") + return RedirectResponse("/game/mai2/", 303) + + elif uri == "/game/mai2/rival.delete": + rival_id = frm.get("rivalUserId") + await self.data.profile.remove_rival(usr_sesh.user_id, rival_id) + # self.logger.info(f"{response}") + return RedirectResponse("/game/mai2/", 303) + + elif uri == "/game/mai2/rival.show": + rival_id = frm.get("rivalUserId") + show = frm.get("showRival", "false") == "true" + await self.data.profile.set_rival_shown(usr_sesh.user_id, rival_id, show) + return RedirectResponse("/game/mai2/", 303) \ No newline at end of file diff --git a/titles/mai2/templates/mai2_index.jinja b/titles/mai2/templates/mai2_index.jinja index 6490fdc..50ebb72 100644 --- a/titles/mai2/templates/mai2_index.jinja +++ b/titles/mai2/templates/mai2_index.jinja @@ -17,6 +17,10 @@ + + ID: + {{ profile.user }} + version: @@ -86,6 +90,40 @@ +
+
+ + + + + + + + + + {% for rival in rival_list %} + + + + + + + + {% endfor %} +
+ RIVALS + +
IdNameRatingShow
{{ rival.2 }}{{ rival.0 }}{{ rival.1 }} +
+ +
+
+ +
+
{% if error is defined %} @@ -120,6 +158,75 @@ + +{% for rival in rival_list %} + +{% endfor %} +{% for rival in rival_list %} + +{% endfor %} +{% for rival in rival_list %} + +{% endfor %} {% endblock content %} \ No newline at end of file From de8294820a973466dc362545fd030eccc5f330a2 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 1 Aug 2025 00:54:48 +0800 Subject: [PATCH 31/63] UserFriendRegist Bugfix --- titles/mai2/buddiesplus.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/titles/mai2/buddiesplus.py b/titles/mai2/buddiesplus.py index fc0bcfb..6ed0025 100644 --- a/titles/mai2/buddiesplus.py +++ b/titles/mai2/buddiesplus.py @@ -75,10 +75,10 @@ class Mai2BuddiesPlus(Mai2Buddies): user1rivalList = await self.data.profile.get_rivals(data["userId1"]) or [] user2rivalList = await self.data.profile.get_rivals(data["userId2"]) or [] - is_user2_in_user1_rivals = any(rival["rival"] == data["userId2"] for rival in user1rivalList) - is_user1_in_user2_rivals = any(rival["rival"] == data["userId1"] for rival in user2rivalList) - user1_show_count = sum(1 for rival in user1rivalList if rival.get("show") == 1) - user2_show_count = sum(1 for rival in user2rivalList if rival.get("show") == 1) + is_user2_in_user1_rivals = any(row.rival == data["userId2"] for row in user1rivalList) + is_user1_in_user2_rivals = any(row.rival == data["userId1"] for row in user2rivalList) + user1_show_count = sum(1 for row in user1rivalList if row.show is True) + user2_show_count = sum(1 for row in user2rivalList if row.show is True) # initialize returnCode returnCode1 = 2 @@ -95,20 +95,12 @@ class Mai2BuddiesPlus(Mai2Buddies): # Case2 has single rival elif is_user2_in_user1_rivals != is_user1_in_user2_rivals: - if is_user2_in_user1_rivals and not is_user1_in_user2_rivals: - if user1_show_count >= 3 and user2_show_count >= 3: - returnCode1, returnCode2 = 1, 1 - elif user1_show_count >= 3: - returnCode1, returnCode2 = 1, 2 - elif user2_show_count >= 3: - returnCode1, returnCode2 = 2, 1 - else: - if user1_show_count >= 3 and user2_show_count >= 3: - returnCode1, returnCode2 = 1, 1 - elif user1_show_count >= 3: - returnCode1, returnCode2 = 1, 2 - elif user2_show_count >= 3: - returnCode1, returnCode2 = 2, 1 + if user1_show_count >= 3 and user2_show_count >= 3: + returnCode1, returnCode2 = 1, 1 + elif user1_show_count >= 3: + returnCode1, returnCode2 = 1, 2 + elif user2_show_count >= 3: + returnCode1, returnCode2 = 2, 1 # execute add_rival and show_rival if not is_user2_in_user1_rivals: From 2d61646307493fd2be89e01dc37b9ad044f0e379 Mon Sep 17 00:00:00 2001 From: ppc Date: Wed, 6 Aug 2025 18:04:40 +0100 Subject: [PATCH 32/63] support removing favourite music entires --- titles/mai2/dx.py | 6 ++--- titles/mai2/schema/item.py | 50 +++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9b8b547..498aabb 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -258,9 +258,9 @@ class Mai2DX(Mai2Base): if kind_id is not None: await self.data.item.put_favorite(user_id, kind_id, fav["itemIdList"]) - if "userFavoritemusicList" in upsert and len(upsert["userFavoritemusicList"]) > 0: - for fav in upsert["userFavoritemusicList"]: - await self.data.item.add_fav_music(user_id, fav["id"], fav["orderId"]) + # added in BUDDiES+ + if "isNewFavoritemusicList" in upsert and upsert["isNewFavoritemusicList"] != "" and "userFavoritemusicList" in upsert: + await self.data.item.put_fav_music(user_id, ((fav["id"], fav["orderId"]) for fav in upsert["userFavoritemusicList"])) if ( "userFriendSeasonRankingList" in upsert diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 8639ae5..bb35756 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -1,7 +1,8 @@ +from collections.abc import Iterable from datetime import datetime from typing import Dict, List, Optional -from sqlalchemy import Column, Table, UniqueConstraint, and_, or_ +from sqlalchemy import Column, Table, UniqueConstraint, and_, or_, not_ from sqlalchemy.dialects.mysql import insert from sqlalchemy.engine import Row from sqlalchemy.schema import ForeignKey @@ -550,25 +551,36 @@ class Mai2ItemData(BaseData): if result: return result.fetchall() - async def add_fav_music(self, user_id: int, music_id: int, order_id: Optional[int] = None) -> Optional[int]: - sql = insert(fav_music).values( - user = user_id, - musicId = music_id, - orderId = order_id - ) + async def put_fav_music(self, user_id: int, fav_list: Iterable[tuple[int, Optional[int]]]) -> Optional[int]: + row_count = 0 + processed_music_ids = [] + + for music_id, order_id in fav_list: + sql = insert(fav_music).values( + user = user_id, + musicId = music_id, + orderId = order_id + ) + + conflict = sql.on_duplicate_key_update(orderId = order_id) + result = await self.execute(conflict) + + processed_music_ids.append(music_id) + + if not result: + self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") + continue + + row_count += result.rowcount + + clear_stale_entries_stmt = fav_music.delete(and_(fav_music.c.user == user_id, not_(fav_music.c.musicId.in_(processed_music_ids)))) + result = await self.execute(clear_stale_entries_stmt) + + if result is None: + self.logger.error(f"Failed to clear stale favorite music entries for user {user_id}!") + return None - conflict = sql.on_duplicate_key_update(orderId = order_id) - - result = await self.execute(conflict) - if result: - return result.lastrowid - - self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") - - async def remove_fav_music(self, user_id: int, music_id: int) -> None: - result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id))) - if not result: - self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") + return row_count + result.rowcount async def put_card( self, From 9f916a6302cd5de8c12aa60ef72d5945c3c7d30d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 19 Aug 2025 10:54:54 -0400 Subject: [PATCH 33/63] mai2: fix card importer --- titles/mai2/schema/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 29e020e..a12e0fd 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -254,7 +254,7 @@ class Mai2StaticData(BaseData): async def put_card(self, version: int, card_id: int, card_name: str, opt_id: int = None, **card_data) -> int: sql = insert(cards).values( - version=version, cardId=card_id, cardName=card_name, opt=coalesce(cards.c.opt, opt_id) **card_data + version=version, cardId=card_id, cardName=card_name, opt=coalesce(cards.c.opt, opt_id), **card_data ) conflict = sql.on_duplicate_key_update(opt=coalesce(cards.c.opt, opt_id), **card_data) From 91f06ccfd2824e6040872b0bb0d1c830a007aae3 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 23 Mar 2025 18:53:38 +0100 Subject: [PATCH 34/63] chuni: initial verse support --- changelog.md | 3 + .../versions/49c295e89cd4_chunithm_verse.py | 84 ++++++ docs/game_specific_info.md | 1 + example_config/chuni.yaml | 3 + readme.md | 1 + titles/chuni/base.py | 201 +++++++++----- titles/chuni/const.py | 2 + titles/chuni/index.py | 15 +- titles/chuni/new.py | 12 +- titles/chuni/read.py | 33 +++ titles/chuni/schema/item.py | 135 +++++++--- titles/chuni/schema/profile.py | 6 +- titles/chuni/schema/score.py | 4 +- titles/chuni/schema/static.py | 196 ++++++++++++-- titles/chuni/verse.py | 248 ++++++++++++++++++ 15 files changed, 807 insertions(+), 137 deletions(-) create mode 100644 core/data/alembic/versions/49c295e89cd4_chunithm_verse.py create mode 100644 titles/chuni/verse.py diff --git a/changelog.md b/changelog.md index 4620084..b274cc9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,9 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20250803 ++ CHUNITHM VERSE support added + ## 20250327 + O.N.G.E.K.I. bright MEMORY Act.3 support added + CardMaker support updated diff --git a/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py new file mode 100644 index 0000000..61b813f --- /dev/null +++ b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py @@ -0,0 +1,84 @@ +"""CHUNITHM VERSE support + +Revision ID: 49c295e89cd4 +Revises: f6007bbf057d +Create Date: 2025-03-09 14:10:03.067328 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql +from sqlalchemy.sql import func + +# revision identifiers, used by Alembic. +revision = "49c295e89cd4" +down_revision = "f6007bbf057d" +branch_labels = None +depends_on = None + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("chuni_profile_data", sa.Column("trophyIdSub1", sa.Integer())) + op.add_column("chuni_profile_data", sa.Column("trophyIdSub2", sa.Integer())) + op.add_column("chuni_score_playlog", sa.Column("monthPoint", sa.Integer())) + op.add_column("chuni_score_playlog", sa.Column("eventPoint", sa.Integer())) + + op.create_table( + "chuni_static_unlock_challenge", + sa.Column("id", sa.Integer(), primary_key=True, nullable=False), + sa.Column("version", sa.Integer(), nullable=False), + sa.Column("unlockChallengeId", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255)), + sa.Column("isEnabled", sa.Boolean(), server_default="1"), + sa.Column("startDate", sa.TIMESTAMP(), server_default=func.now()), + sa.Column("courseId1", sa.Integer()), + sa.Column("courseId2", sa.Integer()), + sa.Column("courseId3", sa.Integer()), + sa.Column("courseId4", sa.Integer()), + sa.Column("courseId5", sa.Integer()), + sa.UniqueConstraint( + "version", "unlockChallengeId", name="chuni_static_unlock_challenge_uk" + ), + mysql_charset="utf8mb4", + ) + + op.create_tablee( + "chuni_item_unlock_challenge", + sa.Column("id", sa.Integer(), primary_key=True, nullable=False), + sa.Column("version", sa.Integer(), nullable=False), + sa.Column( + "user", + sa.ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + sa.Column("unlockChallengeId", sa.Integer(), nullable=False), + sa.Column("status", sa.Integer()), + sa.Column("clearCourseId", sa.Integer()), + sa.Column("conditionType", sa.Integer()), + sa.Column("score", sa.Integer()), + sa.Column("life", sa.Integer()), + sa.Column("clearDate", sa.Integer()), + sa.UniqueConstraint( + "version", + "user", + "unlockChallengeId", + name="chuni_item_unlock_challenge_uk", + ), + mysql_charset="utf8mb4", + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("chuni_score_playlog", "eventPoint") + op.drop_column("chuni_score_playlog", "monthPoint") + op.drop_column("chuni_profile_data", "trophyIdSub2") + op.drop_column("chuni_profile_data", "trophyIdSub1") + + op.drop_table("chuni_static_unlock_challenge") + op.drop_table("chuni_item_unlock_challenge") + # ### end Alembic commands ### diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index e05a771..21faf07 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -68,6 +68,7 @@ Games listed below have been tested and confirmed working. | 14 | CHUNITHM SUN PLUS | | 15 | CHUNITHM LUMINOUS | | 16 | CHUNITHM LUMINOUS PLUS | +| 17 | CHUNITHM VERSE | ### Importer diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index e2ae746..f9c2f20 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -44,6 +44,9 @@ version: 16: rom: 2.25.00 data: 2.25.00 + 17: + rom: 2.30.00 + data: 2.30.00 crypto: encrypted_only: False diff --git a/readme.md b/readme.md index 8591c74..527414a 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + SUN PLUS + LUMINOUS + LUMINOUS PLUS + + VERSE + crossbeats REV. + Crossbeats REV. diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 9333aab..d28a799 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -53,7 +53,9 @@ class ChuniBase: if not self.game_cfg.mods.use_login_bonus: return {"returnCode": 1} - login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version) + login_bonus_presets = await self.data.static.get_login_bonus_presets( + self.version + ) for preset in login_bonus_presets: # check if a user already has some pogress and if not add the @@ -197,15 +199,21 @@ class ChuniBase: async def handle_get_game_message_api_request(self, data: Dict) -> Dict: return { - "type": data["type"], - "length": 1, - "gameMessageList": [{ - "id": 1, - "type": 1, - "message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_cfg.server.news_msg else self.game_cfg.server.news_msg, - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }] + "type": data["type"], + "length": 1, + "gameMessageList": [ + { + "id": 1, + "type": 1, + "message": ( + f"Welcome to {self.core_cfg.server.name} network!" + if not self.game_cfg.server.news_msg + else self.game_cfg.server.news_msg + ), + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + } + ], } async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: @@ -217,7 +225,10 @@ class ChuniBase: async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago - if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + if ( + self.core_cfg.title.reboot_start_time == "" + or self.core_cfg.title.reboot_end_time == "" + ): reboot_start = datetime.strftime( datetime.utcnow() + timedelta(hours=6), self.date_time_format ) @@ -226,15 +237,29 @@ class ChuniBase: ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() # parse config start/end times into datetime - reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") - reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + reboot_start_time = datetime.strptime( + self.core_cfg.title.reboot_start_time, "%H:%M" + ) + reboot_end_time = datetime.strptime( + self.core_cfg.title.reboot_end_time, "%H:%M" + ) # offset datetimes with current date/time - reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) - reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_start_time = reboot_start_time.replace( + year=current_jst.year, + month=current_jst.month, + day=current_jst.day, + tzinfo=pytz.timezone("Asia/Tokyo"), + ) + reboot_end_time = reboot_end_time.replace( + year=current_jst.year, + month=current_jst.month, + day=current_jst.day, + tzinfo=pytz.timezone("Asia/Tokyo"), + ) # create strings for use in gameSetting reboot_start = reboot_start_time.strftime(self.date_time_format) @@ -255,6 +280,7 @@ class ChuniBase: "isDumpUpload": "false", "isAou": "false", } + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: user_activity_list = await self.data.profile.get_profile_activity( data["userId"], data["kind"] @@ -285,7 +311,7 @@ class ChuniBase: rows = await self.data.item.get_characters( user_id, limit=max_ct + 1, offset=next_idx ) - + if rows is None or len(rows) == 0: return { "userId": user_id, @@ -335,7 +361,7 @@ class ChuniBase: return { "userId": data["userId"], "length": 0, - "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint + "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint } async def handle_get_user_course_api_request(self, data: Dict) -> Dict: @@ -421,15 +447,9 @@ class ChuniBase: p = await self.data.profile.get_rival(data["rivalId"]) if p is None: return {} - userRivalData = { - "rivalId": p.user, - "rivalName": p.userName - } - return { - "userId": data["userId"], - "userRivalData": userRivalData - } - + userRivalData = {"rivalId": p.user, "rivalName": p.userName} + return {"userId": data["userId"], "userRivalData": userRivalData} + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: user_id = int(data["userId"]) rival_id = int(data["rivalId"]) @@ -459,18 +479,25 @@ class ChuniBase: # note that itertools.groupby will only work on sorted keys, which is already sorted by # the query in get_scores - for music_id, details_iter in itertools.groupby(music_details, key=lambda x: x["musicId"]): + for music_id, details_iter in itertools.groupby( + music_details, key=lambda x: x["musicId"] + ): details: list[dict[Any, Any]] = [ - {"level": d["level"], "scoreMax": d["scoreMax"]} - for d in details_iter + {"level": d["level"], "scoreMax": d["scoreMax"]} for d in details_iter ] - music_list.append({"musicId": music_id, "length": len(details), "userRivalMusicDetailList": details}) + music_list.append( + { + "musicId": music_id, + "length": len(details), + "userRivalMusicDetailList": details, + } + ) returned_music_details_count += len(details) if len(music_list) >= max_ct: break - + # if we returned fewer PBs than we originally asked for from the database, that means # we queried for the PBs of max_ct + 1 songs. if returned_music_details_count < len(rows): @@ -485,7 +512,7 @@ class ChuniBase: "nextIndex": next_idx, "userRivalMusicList": music_list, } - + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_id = int(data["userId"]) next_idx = int(data["nextIndex"]) @@ -571,7 +598,9 @@ class ChuniBase: async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] - user_login_bonus = await self.data.item.get_all_login_bonus(user_id, self.version) + user_login_bonus = await self.data.item.get_all_login_bonus( + user_id, self.version + ) # ignore the loginBonus request if its disabled in config if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} @@ -621,7 +650,7 @@ class ChuniBase: rows = await self.data.score.get_scores( user_id, limit=max_ct + 1, offset=next_idx ) - + if rows is None or len(rows) == 0: return { "userId": user_id, @@ -636,7 +665,9 @@ class ChuniBase: # note that itertools.groupby will only work on sorted keys, which is already sorted by # the query in get_scores - for _music_id, details_iter in itertools.groupby(music_details, key=lambda x: x["musicId"]): + for _music_id, details_iter in itertools.groupby( + music_details, key=lambda x: x["musicId"] + ): details: list[dict[Any, Any]] = [] for d in details_iter: @@ -650,14 +681,14 @@ class ChuniBase: if len(music_list) >= max_ct: break - + # if we returned fewer PBs than we originally asked for from the database, that means # we queried for the PBs of max_ct + 1 songs. if returned_music_details_count < len(rows): next_idx += max_ct else: next_idx = -1 - + return { "userId": user_id, "length": len(music_list), @@ -687,7 +718,9 @@ class ChuniBase: return bytes([ord(c) for c in src]).decode("utf-8") async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_preview(data["userId"], self.version) + profile = await self.data.profile.get_profile_preview( + data["userId"], self.version + ) if profile is None: return None profile_character = await self.data.item.get_character( @@ -729,7 +762,9 @@ class ChuniBase: } async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recent_rating_list = await self.data.profile.get_profile_recent_rating(data["userId"]) + recent_rating_list = await self.data.profile.get_profile_recent_rating( + data["userId"] + ) if recent_rating_list is None: return { "userId": data["userId"], @@ -762,7 +797,7 @@ class ChuniBase: profile = await self.data.profile.get_profile_data(data["userId"], self.version) if profile is None: - return {"userId": data["userId"], "teamId": 0} + return {"userId": data["userId"], "teamId": 0} if profile and profile["teamId"]: # Get team by id @@ -787,7 +822,7 @@ class ChuniBase: "teamId": team_id, "teamRank": team_rank, "teamName": team_name, - "assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost? + "assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost? "userTeamPoint": { "userId": data["userId"], "teamId": team_id, @@ -796,7 +831,7 @@ class ChuniBase: "aggrDate": data["playDate"], }, } - + async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], @@ -805,7 +840,9 @@ class ChuniBase: "teamCourseSettingList": [], } - async def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: + async def handle_get_team_course_setting_api_request_proto( + self, data: Dict + ) -> Dict: return { "userId": data["userId"], "length": 1, @@ -820,11 +857,11 @@ class ChuniBase: "teamCourseMusicList": [ {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, - {"track": 184, "type": 1, "level": 3, "selectLevel": -1} + {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, ], "teamCourseRankingInfoList": [], "recodeDate": "2099-12-31 11:59:99.0", - "isPlayed": False + "isPlayed": False, } ], } @@ -834,7 +871,7 @@ class ChuniBase: "userId": data["userId"], "length": 0, "nextIndex": -1, - "teamCourseRuleList": [] + "teamCourseRuleList": [], } async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: @@ -849,7 +886,7 @@ class ChuniBase: "damageMiss": 1, "damageAttack": 1, "damageJustice": 1, - "damageJusticeC": 1 + "damageJusticeC": 1, } ], } @@ -860,7 +897,7 @@ class ChuniBase: if int(user_id) & 0x1000000000001 == 0x1000000000001: place_id = int(user_id) & 0xFFFC00000000 - + self.logger.info("Guest play from place ID %d, ignoring.", place_id) return {"returnCode": "1"} @@ -882,7 +919,9 @@ class ChuniBase: ) if "userGameOption" in upsert: - await self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) + await self.data.profile.put_profile_option( + user_id, upsert["userGameOption"][0] + ) if "userGameOptionEx" in upsert: await self.data.profile.put_profile_option_ex( @@ -929,33 +968,41 @@ class ChuniBase: for playlog in upsert["userPlaylogList"]: # convert the player names to utf-8 if playlog["playedUserName1"] is not None: - playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"]) + playlog["playedUserName1"] = self.read_wtf8( + playlog["playedUserName1"] + ) if playlog["playedUserName2"] is not None: - playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) + playlog["playedUserName2"] = self.read_wtf8( + playlog["playedUserName2"] + ) if playlog["playedUserName3"] is not None: - playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) + playlog["playedUserName3"] = self.read_wtf8( + playlog["playedUserName3"] + ) await self.data.score.put_playlog(user_id, playlog, self.version) if "userTeamPoint" in upsert: team_points = upsert["userTeamPoint"] try: for tp in team_points: - if tp["teamId"] != '65535': + if tp["teamId"] != "65535": # Fetch the current team data - current_team = await self.data.profile.get_team_by_id(tp["teamId"]) + current_team = await self.data.profile.get_team_by_id( + tp["teamId"] + ) # Calculate the new teamPoint - new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"] + new_team_point = ( + int(tp["teamPoint"]) + current_team["teamPoint"] + ) # Prepare the data to update - team_data = { - "teamPoint": new_team_point - } + team_data = {"teamPoint": new_team_point} # Update the team data await self.data.profile.update_team(tp["teamId"], team_data) except: - pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass + pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: await self.data.item.put_map_area(user_id, map_area) @@ -973,22 +1020,28 @@ class ChuniBase: await self.data.item.put_login_bonus( user_id, self.version, login["presetId"], isWatched=True ) - - if "userRecentPlayerList" in upsert: # TODO: Seen in Air, maybe implement sometime + + if ( + "userRecentPlayerList" in upsert + ): # TODO: Seen in Air, maybe implement sometime for rp in upsert["userRecentPlayerList"]: pass - for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}: + for rating_type in { + "userRatingBaseList", + "userRatingBaseHotList", + "userRatingBaseNextList", + }: if rating_type not in upsert: continue - + await self.data.profile.put_profile_rating( user_id, self.version, rating_type, upsert[rating_type], ) - + # added in LUMINOUS if "userCMissionList" in upsert: for cmission in upsert["userCMissionList"]: @@ -1003,7 +1056,9 @@ class ChuniBase: ) for progress in cmission["userCMissionProgressList"]: - await self.data.item.put_cmission_progress(user_id, mission_id, progress) + await self.data.item.put_cmission_progress( + user_id, mission_id, progress + ) if "userNetBattleData" in upsert: net_battle = upsert["userNetBattleData"][0] @@ -1035,10 +1090,20 @@ class ChuniBase: added_ids = music_ids - keep_ids for fav_id in deleted_ids: - await self.data.item.delete_favorite_music(user_id, self.version, fav_id) - + await self.data.item.delete_favorite_music( + user_id, self.version, fav_id + ) + 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"]: + await self.data.item.put_unlock_challenge( + user_id, self.version, unlock_challenge + ) + return {"returnCode": "1"} diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 7c534d3..f5642e3 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -28,6 +28,7 @@ class ChuniConstants: VER_CHUNITHM_SUN_PLUS = 14 VER_CHUNITHM_LUMINOUS = 15 VER_CHUNITHM_LUMINOUS_PLUS = 16 + VER_CHUNITHM_VERSE = 17 VERSION_NAMES = [ "CHUNITHM", @@ -47,6 +48,7 @@ class ChuniConstants: "CHUNITHM SUN PLUS", "CHUNITHM LUMINOUS", "CHUNITHM LUMINOUS PLUS", + "CHUNITHM VERSE" ] SCORE_RANK_INTERVALS_OLD = [ diff --git a/titles/chuni/index.py b/titles/chuni/index.py index cc25289..f792e6b 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -37,6 +37,7 @@ from .sun import ChuniSun from .sunplus import ChuniSunPlus from .luminous import ChuniLuminous from .luminousplus import ChuniLuminousPlus +from .verse import ChuniVerse class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -66,6 +67,7 @@ class ChuniServlet(BaseServlet): ChuniSunPlus, ChuniLuminous, ChuniLuminousPlus, + ChuniVerse ] self.logger = logging.getLogger("chuni") @@ -113,6 +115,7 @@ class ChuniServlet(BaseServlet): f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8, f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8, ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56, + ChuniConstants.VER_CHUNITHM_VERSE: 42, } for version, keys in self.game_cfg.crypto.keys.items(): @@ -218,9 +221,9 @@ class ChuniServlet(BaseServlet): ] async def render_POST(self, request: Request) -> bytes: - endpoint: str = request.path_params.get('endpoint') - version: int = request.path_params.get('version') - game_code: str = request.path_params.get('game') + endpoint: str = request.path_params.get("endpoint") + version: int = request.path_params.get("version") + game_code: str = request.path_params.get("game") if endpoint.lower() == "ping": return Response(zlib.compress(b'{"returnCode": "1"}')) @@ -264,8 +267,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS elif version >= 220 and version < 225: # LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS - elif version >= 225: # LUMINOUS PLUS + elif version >= 225 and version < 230: # LUMINOUS PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS + elif version >= 230: # VERSE + internal_ver = ChuniConstants.VER_CHUNITHM_VERSE elif game_code == "SDGS": # Int if version < 105: # SUPERSTAR internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS @@ -380,7 +385,7 @@ class ChuniServlet(BaseServlet): handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): - self.logger.warning(f"Unhandled v{version} request {endpoint}") + self.logger.warning(f"Unhandled v{version} request {func_to_find}") resp = {"returnCode": 1} else: diff --git a/titles/chuni/new.py b/titles/chuni/new.py index a3aa1a3..b39efc8 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -28,16 +28,18 @@ class ChuniNew(ChuniBase): def _interal_ver_to_intver(self) -> str: if self.version == ChuniConstants.VER_CHUNITHM_NEW: return "200" - if self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS: + elif self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS: return "205" - if self.version == ChuniConstants.VER_CHUNITHM_SUN: + elif self.version == ChuniConstants.VER_CHUNITHM_SUN: return "210" - if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: + elif self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: return "215" - if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: + elif self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: return "220" - if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: + elif self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: return "225" + elif self.version == ChuniConstants.VER_CHUNITHM_VERSE: + return "230" async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9 diff --git a/titles/chuni/read.py b/titles/chuni/read.py index bd6ff07..47c1939 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -62,6 +62,7 @@ class ChuniReader(BaseReader): await self.read_character(f"{dir}/chara", dds_images, 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_unlock_challenge(f"{dir}/unlockChallenge") 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"): @@ -499,6 +500,38 @@ class ChuniReader(BaseReader): self.logger.info(f"Opt folder {opt_folder} (Database ID {opt_id}) contains {data_config['Version']['Name']} v{data_config['Version']['VerMajor']}.{data_config['Version']['VerMinor']}.{opt_seq}") return opt_id + async def read_unlock_challenge(self, uc_dir: str) -> None: + for root, dirs, files in walk(uc_dir): + for dir in dirs: + if path.exists(f"{root}/{dir}/UnlockChallenge.xml"): + with open(f"{root}/{dir}/UnlockChallenge.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/UnlockChallengeMusicListSubData/unlockChallengeMusicData/courseList/list").findall("UnlockChallengeCourseListSubData"): + course_id = course.find("unlockChallengeCourseData/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_unlock_challenge( + self.version, id, name, + **course_kwargs + ) + if result is not None: + self.logger.info(f"Inserted unlock challenge {id}") + else: + self.logger.warning(f"Failed to unlock challenge {id}") + def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None: # Convert the image to png so we can easily display it in the frontend file_src = path.join(src_dir, filename) diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 93dcf86..84c6364 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -262,7 +262,11 @@ cmission_progress = Table( "chuni_item_cmission_progress", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("missionId", Integer, nullable=False), Column("order", Integer), Column("stage", Integer), @@ -273,14 +277,34 @@ cmission_progress = Table( mysql_charset="utf8mb4", ) +unlock_challenge = Table( + "chuni_item_unlock_challenge", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("unlockChallengeId", Integer, nullable=False), + Column("status", Integer), + Column("clearCourseId", Integer), + Column("conditionType", Integer), + Column("score", Integer), + Column("life", Integer), + Column("clearDate", TIMESTAMP), + UniqueConstraint( + "version", "user", "unlockChallengeId", name="chuni_item_unlock_challenge_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): async def get_oldest_free_matching(self, version: int) -> Optional[Row]: sql = matching.select( - and_( - matching.c.version == version, - matching.c.isFull == False - ) + and_(matching.c.version == version, matching.c.isFull == False) ).order_by(matching.c.roomId.asc()) result = await self.execute(sql) @@ -289,11 +313,9 @@ class ChuniItemData(BaseData): return result.fetchone() async def get_newest_matching(self, version: int) -> Optional[Row]: - sql = matching.select( - and_( - matching.c.version == version - ) - ).order_by(matching.c.roomId.desc()) + sql = matching.select(and_(matching.c.version == version)).order_by( + matching.c.roomId.desc() + ) result = await self.execute(sql) if result is None: @@ -301,11 +323,7 @@ class ChuniItemData(BaseData): return result.fetchone() async def get_all_matchings(self, version: int) -> Optional[List[Row]]: - sql = matching.select( - and_( - matching.c.version == version - ) - ) + sql = matching.select(and_(matching.c.version == version)) result = await self.execute(sql) if result is None: @@ -329,7 +347,7 @@ class ChuniItemData(BaseData): matching_member_info_list: List, user_id: int = None, rest_sec: int = 60, - is_full: bool = False + is_full: bool = False, ) -> Optional[int]: sql = insert(matching).values( roomId=room_id, @@ -452,23 +470,31 @@ class ChuniItemData(BaseData): return None return result.fetchone() - async def put_favorite_music(self, user_id: int, version: int, music_id: int) -> Optional[int]: - sql = insert(favorite).values(user=user_id, version=version, favId=music_id, favKind=1) + async def put_favorite_music( + self, user_id: int, version: int, music_id: int + ) -> Optional[int]: + sql = insert(favorite).values( + user=user_id, version=version, favId=music_id, favKind=1 + ) - conflict = sql.on_duplicate_key_update(user=user_id, version=version, favId=music_id, favKind=1) + conflict = sql.on_duplicate_key_update( + user=user_id, version=version, favId=music_id, favKind=1 + ) result = await self.execute(conflict) if result is None: return None return result.lastrowid - async def delete_favorite_music(self, user_id: int, version: int, music_id: int) -> Optional[int]: + async def delete_favorite_music( + self, user_id: int, version: int, music_id: int + ) -> Optional[int]: sql = delete(favorite).where( and_( - favorite.c.user==user_id, - favorite.c.version==version, - favorite.c.favId==music_id, - favorite.c.favKind==1 + favorite.c.user == user_id, + favorite.c.version == version, + favorite.c.favId == music_id, + favorite.c.favKind == 1, ) ) @@ -611,8 +637,12 @@ class ChuniItemData(BaseData): return None return result.lastrowid - async def get_map_areas(self, user_id: int, map_area_ids: List[int]) -> Optional[List[Row]]: - sql = select(map_area).where(map_area.c.user == user_id, map_area.c.mapAreaId.in_(map_area_ids)) + async def get_map_areas( + self, user_id: int, map_area_ids: List[int] + ) -> Optional[List[Row]]: + sql = select(map_area).where( + map_area.c.user == user_id, map_area.c.mapAreaId.in_(map_area_ids) + ) result = await self.execute(sql) if result is None: @@ -713,7 +743,7 @@ class ChuniItemData(BaseData): ) return None return result.lastrowid - + async def put_cmission_progress( self, user_id: int, mission_id: int, progress_data: Dict ) -> Optional[int]: @@ -723,10 +753,10 @@ class ChuniItemData(BaseData): sql = insert(cmission_progress).values(**progress_data) conflict = sql.on_duplicate_key_update(**progress_data) result = await self.execute(conflict) - + if result is None: return None - + return result.lastrowid async def get_cmission_progress( @@ -739,21 +769,21 @@ class ChuniItemData(BaseData): ) ).order_by(cmission_progress.c.order.asc()) result = await self.execute(sql) - + if result is None: return None - + return result.fetchall() - + async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]: sql = cmission.select( and_(cmission.c.user == user_id, cmission.c.missionId == mission_id) ) result = await self.execute(sql) - + if result is None: return None - + return result.fetchone() async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]: @@ -762,17 +792,46 @@ class ChuniItemData(BaseData): sql = insert(cmission).values(**mission_data) conflict = sql.on_duplicate_key_update(**mission_data) result = await self.execute(conflict) - + if result is None: return None - + return result.lastrowid async def get_cmissions(self, user_id: int) -> Optional[List[Row]]: sql = cmission.select(cmission.c.user == user_id) result = await self.execute(sql) - + + if result is None: + return None + + return result.fetchall() + + async def put_unlock_challenge( + self, user_id: int, version: int, unlock_challenge_data: Dict + ) -> Optional[int]: + unlock_challenge_data["user"] = user_id + unlock_challenge_data["version"] = version + + sql = insert(unlock_challenge).values(**unlock_challenge_data) + conflict = sql.on_duplicate_key_update(**unlock_challenge_data) + + result = await self.execute(conflict) + if result is None: + return None + return result.lastrowid + + async def get_unlock_challenges( + self, user_id: int, version: int + ) -> Optional[List[Row]]: + sql = unlock_challenge.select( + and_( + unlock_challenge.c.user == user_id, + unlock_challenge.c.version == version, + ) + ) + + result = await self.execute(sql) if result is None: return None - return result.fetchall() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index c7fb750..5f54f5e 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -25,6 +25,8 @@ profile = Table( Column("frameId", Integer), Column("isMaimai", Boolean), Column("trophyId", Integer), + Column("trophyIdSub1", Integer), + Column("trophyIdSub2", Integer), Column("userName", String(25)), Column("isWebJoin", Boolean), Column("playCount", Integer), @@ -465,6 +467,8 @@ class ChuniProfileData(BaseData): sql = profile.update((profile.c.user == user_id) & (profile.c.version == version)).values( nameplateId=new_nameplate, trophyId=new_trophy, + trophyIdSub1=new_trophySub1, + trophyIdSub2=new_trophySub2, charaIllustId=new_character ) result = await self.execute(sql) @@ -899,4 +903,4 @@ class ChuniProfileData(BaseData): async def get_net_battle(self, aime_id: int) -> Optional[Row]: result = await self.execute(net_battle.select(net_battle.c.user == aime_id)) if result: - return result.fetchone() + return result.fetchone() \ No newline at end of file diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 50a8f7f..036e41b 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -139,6 +139,8 @@ playlog = Table( Column("regionId", Integer), Column("machineType", Integer), Column("ticketId", Integer), + Column("monthPoint", Integer), + Column("eventPoint", Integer), mysql_charset="utf8mb4" ) @@ -420,4 +422,4 @@ class ChuniScoreData(BaseData): return None rows = result.fetchall() - return [dict(row) for row in rows] + return [dict(row) for row in rows] \ No newline at end of file diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index f4f0f9f..0e0f2ca 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -289,6 +289,27 @@ login_bonus = Table( mysql_charset="utf8mb4", ) +unlock_challenge = Table( + "chuni_static_unlock_challenge", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("unlockChallengeId", Integer, nullable=False), + Column("name", String(255)), + Column("isEnabled", Boolean, server_default="1"), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("courseId1", Integer), + Column("courseId2", Integer), + Column("courseId3", Integer), + Column("courseId4", Integer), + Column("courseId5", Integer), + UniqueConstraint( + "version", "unlockChallengeId", name="chuni_static_unlock_challenge_pk" + ), + mysql_charset="utf8mb4", +) + + class ChuniStaticData(BaseData): async def put_login_bonus( self, @@ -556,7 +577,7 @@ class ChuniStaticData(BaseData): return result.fetchall() async def get_music(self, version: int) -> Optional[List[Row]]: - sql = music.select(music.c.version <= version) + sql = music.select(music.c.version == version) result = await self.execute(sql) if result is None: @@ -586,6 +607,28 @@ class ChuniStaticData(BaseData): if result is None: return None return result.fetchone() + + async def get_music_by_metadata( + self, title: Optional[str] = None, artist: Optional[str] = None, genre: Optional[str] = None + ) -> Optional[List[Row]]: + # all conditions should use like for partial matches + conditions = [] + if title: + conditions.append(music.c.title.like(f"%{title}%")) + if artist: + conditions.append(music.c.artist.like(f"%{artist}%")) + if genre: + conditions.append(music.c.genre.like(f"%{genre}%")) + + if not conditions: + return None + + sql = select(music).where(and_(*conditions)) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() async def put_avatar( self, @@ -629,11 +672,25 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_avatar_items(self, version: int, category: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_avatar_items( + self, version: int, category: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(avatar).where((avatar.c.version == version) & (avatar.c.category == category) & (avatar.c.isEnabled)).order_by(avatar.c.sortName) + sql = ( + select(avatar) + .where( + (avatar.c.version == version) + & (avatar.c.category == category) + & (avatar.c.isEnabled) + ) + .order_by(avatar.c.sortName) + ) else: - sql = select(avatar).where((avatar.c.version == version) & (avatar.c.category == category)).order_by(avatar.c.sortName) + sql = ( + select(avatar) + .where((avatar.c.version == version) & (avatar.c.category == category)) + .order_by(avatar.c.sortName) + ) result = await self.execute(sql) if result is None: @@ -676,11 +733,21 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_nameplates(self, version: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_nameplates( + self, version: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(nameplate).where((nameplate.c.version == version) & (nameplate.c.isEnabled)).order_by(nameplate.c.sortName) + sql = ( + select(nameplate) + .where((nameplate.c.version == version) & (nameplate.c.isEnabled)) + .order_by(nameplate.c.sortName) + ) else: - sql = select(nameplate).where(nameplate.c.version == version).order_by(nameplate.c.sortName) + sql = ( + select(nameplate) + .where(nameplate.c.version == version) + .order_by(nameplate.c.sortName) + ) result = await self.execute(sql) if result is None: @@ -720,11 +787,21 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_trophies(self, version: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_trophies( + self, version: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(trophy).where((trophy.c.version == version) & (trophy.c.isEnabled)).order_by(trophy.c.name) + sql = ( + select(trophy) + .where((trophy.c.version == version) & (trophy.c.isEnabled)) + .order_by(trophy.c.name) + ) else: - sql = select(trophy).where(trophy.c.version == version).order_by(trophy.c.name) + sql = ( + select(trophy) + .where(trophy.c.version == version) + .order_by(trophy.c.name) + ) result = await self.execute(sql) if result is None: @@ -767,11 +844,21 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_map_icons(self, version: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_map_icons( + self, version: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(map_icon).where((map_icon.c.version == version) & (map_icon.c.isEnabled)).order_by(map_icon.c.sortName) + sql = ( + select(map_icon) + .where((map_icon.c.version == version) & (map_icon.c.isEnabled)) + .order_by(map_icon.c.sortName) + ) else: - sql = select(map_icon).where(map_icon.c.version == version).order_by(map_icon.c.sortName) + sql = ( + select(map_icon) + .where(map_icon.c.version == version) + .order_by(map_icon.c.sortName) + ) result = await self.execute(sql) if result is None: @@ -814,11 +901,21 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_system_voices(self, version: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_system_voices( + self, version: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(system_voice).where((system_voice.c.version == version) & (system_voice.c.isEnabled)).order_by(system_voice.c.sortName) + sql = ( + select(system_voice) + .where((system_voice.c.version == version) & (system_voice.c.isEnabled)) + .order_by(system_voice.c.sortName) + ) else: - sql = select(system_voice).where(system_voice.c.version == version).order_by(system_voice.c.sortName) + sql = ( + select(system_voice) + .where(system_voice.c.version == version) + .order_by(system_voice.c.sortName) + ) result = await self.execute(sql) if result is None: @@ -873,11 +970,21 @@ class ChuniStaticData(BaseData): return None return result.lastrowid - async def get_characters(self, version: int, enabled_only: bool = True) -> Optional[List[Dict]]: + async def get_characters( + self, version: int, enabled_only: bool = True + ) -> Optional[List[Dict]]: if enabled_only: - sql = select(character).where((character.c.version == version) & (character.c.isEnabled)).order_by(character.c.sortName) + sql = ( + select(character) + .where((character.c.version == version) & (character.c.isEnabled)) + .order_by(character.c.sortName) + ) else: - sql = select(character).where(character.c.version == version).order_by(character.c.sortName) + sql = ( + select(character) + .where(character.c.version == version) + .order_by(character.c.sortName) + ) result = await self.execute(sql) if result is None: @@ -1074,3 +1181,54 @@ class ChuniStaticData(BaseData): self.logger.error(f"Failed to set opt enabled status to {enabled} for opt {opt_id}") return False return True + + + async def put_unlock_challenge( + self, + version: int, + unlock_challenge_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(unlock_challenge).values( + version=version, + unlockChallengeId=unlock_challenge_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_unlock_challenges(self, version: int) -> Optional[List[Dict]]: + sql = unlock_challenge.select( + and_( + unlock_challenge.c.version == version, + unlock_challenge.c.isEnabled == True, + ) + ).order_by(unlock_challenge.c.startDate.asc()) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/chuni/verse.py b/titles/chuni/verse.py new file mode 100644 index 0000000..31138b6 --- /dev/null +++ b/titles/chuni/verse.py @@ -0,0 +1,248 @@ +from datetime import datetime, timedelta +from typing import Dict, List, Set + +from core.config import CoreConfig +from titles.chuni.config import ChuniConfig +from titles.chuni.const import ( + ChuniConstants, + MapAreaConditionLogicalOperator, + MapAreaConditionType, +) +from titles.chuni.luminousplus import ChuniLuminousPlus + + +class ChuniVerse(ChuniLuminousPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_VERSE + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # Does CARD MAKER 1.35 work this far up? + user_data["lastDataVersion"] = "2.30.00" + return user_data + + async def handle_get_game_course_level_api_request(self, data: Dict) -> Dict: + unlock_challenges = await self.data.static.get_unlock_challenges(self.version) + game_course_level_list = [] + + for unlock_challenge in unlock_challenges: + course_ids = [ + unlock_challenge["courseId1"], + unlock_challenge["courseId2"], + unlock_challenge["courseId3"], + unlock_challenge["courseId4"], + unlock_challenge["courseId5"], + ] + + start_date = unlock_challenge["startDate"].replace( + hour=0, minute=0, second=0 + ) + + for i, course_id in enumerate(course_ids): + start = start_date + timedelta(days=7 * i) + end = start_date + timedelta(days=7 * (i + 1)) - timedelta(seconds=1) + + if i == len(course_ids) - 1: + # If this is the last course, set end date to a far future date + end = datetime(2099, 1, 1) + + game_course_level_list.append( + { + "courseId": course_id, + "startDate": start.strftime(self.date_time_format), + "endDate": end.strftime(self.date_time_format), + } + ) + + return { + "length": len(game_course_level_list), + "gameCourseLevelList": game_course_level_list, + } + + async def handle_get_game_uc_condition_api_request(self, data: Dict) -> Dict: + unlock_challenges = await self.data.static.get_unlock_challenges(self.version) + game_unlock_challenge_condition_list = [] + + conditions = { + # unlock Theatore Creatore (ULTIMA) after clearing map VERSE ep. I + 10001: { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020798, + }, + # unlock Crossmythos Rhapsodia after clearing map VERSE ep. IV + 10006: { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020802, + }, + } + + for unlock_challenge in unlock_challenges: + unlock_challenge_id = unlock_challenge["unlockChallengeId"] + + unlock_condition = conditions.get( + unlock_challenge_id, + { + "type": 3, # always unlocked + "conditionId": 0, + }, + ) + + game_unlock_challenge_condition_list.append( + { + "unlockChallengeId": unlock_challenge_id, + "length": 1, + "conditionList": [ + { + "type": unlock_condition["type"], + "conditionId": unlock_condition["conditionId"], + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": unlock_challenge["startDate"].strftime( + self.date_time_format + ), + "endDate": datetime(2099, 1, 1).strftime( + self.date_time_format + ), + } + ], + } + ) + + return { + "length": len(game_unlock_challenge_condition_list), + "gameUnlockChallengeConditionList": game_unlock_challenge_condition_list, + } + + async def handle_get_user_uc_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + + user_unlock_challenges = await self.data.item.get_unlock_challenges( + user_id, self.version + ) + + user_unlock_challenge_list = [ + { + "unlockChallengeId": user_uc["unlockChallengeId"], + "status": user_uc["status"], + "clearCourseId": user_uc["clearCourseId"], + "conditionType": user_uc["conditionType"], + "score": user_uc["score"], + "life": user_uc["life"], + "clearDate": user_uc["clearDate"].strftime(self.date_time_format), + } + for user_uc in user_unlock_challenges + ] + + return { + "userId": user_id, + "userUnlockChallengeList": user_unlock_challenge_list, + } + + async def handle_get_user_rec_music_api_request(self, data: Dict) -> Dict: + rec_limit = 25 # limit for recommendations + user_id = data["userId"] + user_rec_music_set = set() + + recent_rating = await self.data.profile.get_profile_recent_rating(user_id) + if not recent_rating: + # If no recent ratings, return an empty list + return { + "length": 0, + "userRecMusicList": [], + } + + recent_ratings = recent_rating["recentRating"] + # cache music info + music_info_list = [] + + for recent_rating in recent_ratings: + music_id = recent_rating["musicId"] + music_info = await self.data.static.get_song(music_id) + if music_info: + music_info_list.append(music_info) + + # use a set to avoid duplicates + user_rec_music_set = set() + + # try adding recommendations in order of: title → artist → genre + for field in ("title", "artist", "genre"): + await self._add_recommendations(field, user_rec_music_set, music_info_list, rec_limit) + if len(user_rec_music_set) >= rec_limit: + break + + user_rec_music_list = [ + { + "musicId": 1, # no idea + # 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( + f"{music_id},{index + 1}" + for index, music_id in enumerate(user_rec_music_set) + ), + }, + ] + + return { + "length": len(user_rec_music_list), + "userRecMusicList": user_rec_music_list, + } + + async def handle_get_user_rec_rating_api_request(self, data: Dict) -> Dict: + class GetUserRecRatingApi: + class UserRecRating: + ratingMin: int + ratingMax: int + # same as recMusicList in get_user_rec_music_api_request + recMusicList: str + + length: int + userRecRatingList: list[UserRecRating] + + user_id = data["userId"] + + user_rec_rating_list = [] + + return { + "length": len(user_rec_rating_list), + "userRecRatingList": user_rec_rating_list, + } + + async def _add_recommendations( + self, + field: str, + user_rec_music_set: Set[int], + music_info_list: List[Dict], + limit: int = 25, + ) -> None: + """ + Adds music recommendations based on a specific metadata field (title/artist/genre), + excluding music IDs already in the user's recent ratings and recommendations. + """ + # Collect all existing songId to exclude from recommendations + existing_music_ids = { + info["songId"] for info in music_info_list + } + + for music_info in music_info_list: + if len(user_rec_music_set) >= limit: + break + + metadata_value = music_info[field] + if not metadata_value: + continue + + recs = await self.data.static.get_music_by_metadata( + **{field: metadata_value} + ) + for rec in recs or []: + song_id = rec["songId"] + # skip if the song is already in the user's recent ratings + # or if the song is already in the user's recommendations + if ( + len(user_rec_music_set) >= limit + or song_id in existing_music_ids + or song_id in user_rec_music_set + ): + continue + user_rec_music_set.add(song_id) From 3c7ac3ac58c5191b09ef316d3b00a508d6dbbcb7 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 19 Aug 2025 19:25:33 +0200 Subject: [PATCH 35/63] chuni: fix encrypted hash, update unlock challenge req --- titles/chuni/index.py | 185 +++++++++++++++++++++++------------------- titles/chuni/verse.py | 18 ++-- 2 files changed, 112 insertions(+), 91 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index f792e6b..ce4e02e 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -1,20 +1,22 @@ -from starlette.requests import Request -from starlette.routing import Route -from starlette.responses import Response +import asyncio +import re import logging import coloredlogs -from logging.handlers import TimedRotatingFileHandler import zlib import yaml import json import inflection import string +from os import path +from typing import Tuple, Dict, List +from logging.handlers import TimedRotatingFileHandler +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Protocol.KDF import PBKDF2 from Crypto.Hash import SHA1 -from os import path -from typing import Tuple, Dict, List from core import CoreConfig, Utils from core.title import BaseServlet @@ -39,6 +41,7 @@ from .luminous import ChuniLuminous from .luminousplus import ChuniLuminousPlus from .verse import ChuniVerse + class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) @@ -98,15 +101,15 @@ class ChuniServlet(BaseServlet): known_iter_counts = { ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, - f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR + f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR ChuniConstants.VER_CHUNITHM_PARADISE: 44, - f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS + f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS ChuniConstants.VER_CHUNITHM_NEW: 54, f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37, ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, - f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW + f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW ChuniConstants.VER_CHUNITHM_SUN: 70, f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, @@ -126,7 +129,7 @@ class ChuniServlet(BaseServlet): version_idx = version else: version_idx = int(version.split("_")[0]) - + salt = bytes.fromhex(keys[2]) if len(keys) >= 4: @@ -156,12 +159,9 @@ class ChuniServlet(BaseServlet): and version_idx >= ChuniConstants.VER_CHUNITHM_NEW ): method_fixed += "C3Exp" - elif ( - isinstance(version, str) - and version.endswith("_chn") - ): + elif isinstance(version, str) and version.endswith("_chn"): method_fixed += "Chn" - + hash = PBKDF2( method_fixed, salt, @@ -170,7 +170,8 @@ class ChuniServlet(BaseServlet): hmac_hash_module=SHA1, ) - hashed_name = hash.hex()[:32] # truncate unused bytes like the game does + # truncate unused bytes like the game does + hashed_name = hash.hex()[:32] self.hash_table[version][hashed_name] = method_fixed self.logger.debug( @@ -192,7 +193,9 @@ class ChuniServlet(BaseServlet): return True - def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + def get_allnet_info( + self, game_code: str, game_ver: int, keychip: str + ) -> Tuple[str, str]: title_port_int = Utils.get_title_port(self.core_cfg) title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) @@ -206,7 +209,7 @@ class ChuniServlet(BaseServlet): if proto == "https": t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" - else: + else: t_port = f":{title_port_int}" if title_port_int != 80 else "" return ( @@ -216,8 +219,16 @@ class ChuniServlet(BaseServlet): def get_routes(self) -> List[Route]: return [ - Route("/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", self.render_POST, methods=['POST']), - Route("/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", self.render_POST, methods=['POST']), + Route( + "/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", + self.render_POST, + methods=["POST"], + ), + Route( + "/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", + self.render_POST, + methods=["POST"], + ), ] async def render_POST(self, request: Request) -> bytes: @@ -234,67 +245,71 @@ class ChuniServlet(BaseServlet): internal_ver = 0 client_ip = Utils.get_ip_addr(request) - if game_code == "SDHD" or game_code == "SDBT": # JP - if version < 105: # 1.0 - internal_ver = ChuniConstants.VER_CHUNITHM - elif version >= 105 and version < 110: # PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_PLUS - elif version >= 110 and version < 115: # AIR - internal_ver = ChuniConstants.VER_CHUNITHM_AIR - elif version >= 115 and version < 120: # AIR PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS - elif version >= 120 and version < 125: # STAR - internal_ver = ChuniConstants.VER_CHUNITHM_STAR - elif version >= 125 and version < 130: # STAR PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS - elif version >= 130 and version < 135: # AMAZON - internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON - elif version >= 135 and version < 140: # AMAZON PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - elif version >= 140 and version < 145: # CRYSTAL - internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL - elif version >= 145 and version < 150: # CRYSTAL PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 150 and version < 200: # PARADISE - internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 200 and version < 205: # NEW!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 205 and version < 210: # NEW PLUS!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS - elif version >= 210 and version < 215: # SUN - internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 215 and version < 220: # SUN PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS - elif version >= 220 and version < 225: # LUMINOUS - 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 - internal_ver = ChuniConstants.VER_CHUNITHM_VERSE - elif game_code == "SDGS": # Int - if version < 105: # SUPERSTAR - internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 105 and version < 110: # SUPERSTAR PLUS *Cursed but needed due to different encryption key - internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 110 and version < 115: # NEW - internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 115 and version < 120: # NEW PLUS!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS - elif version >= 120 and version < 125: # SUN - internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 125 and version < 130: # SUN PLUS - 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 - internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS - elif game_code == "SDHJ": # Chn - if version < 110: # NEW - internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif 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 - internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS + if game_code == "SDHD" or game_code == "SDBT": # JP + if version < 105: # 1.0 + internal_ver = ChuniConstants.VER_CHUNITHM + elif version >= 105 and version < 110: # PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_PLUS + elif version >= 110 and version < 115: # AIR + internal_ver = ChuniConstants.VER_CHUNITHM_AIR + elif version >= 115 and version < 120: # AIR PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS + elif version >= 120 and version < 125: # STAR + internal_ver = ChuniConstants.VER_CHUNITHM_STAR + elif version >= 125 and version < 130: # STAR PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS + elif version >= 130 and version < 135: # AMAZON + internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON + elif version >= 135 and version < 140: # AMAZON PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS + elif version >= 140 and version < 145: # CRYSTAL + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL + elif version >= 145 and version < 150: # CRYSTAL PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + elif version >= 150 and version < 200: # PARADISE + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE + elif version >= 200 and version < 205: # NEW!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif version >= 205 and version < 210: # NEW PLUS!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 210 and version < 215: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN + elif version >= 215 and version < 220: # SUN PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 220 and version < 225: # LUMINOUS + 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 + internal_ver = ChuniConstants.VER_CHUNITHM_VERSE + elif game_code == "SDGS": # Int + if version < 105: # SUPERSTAR + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + elif ( + version >= 105 and version < 110 + ): # SUPERSTAR PLUS *Cursed but needed due to different encryption key + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE + elif version >= 110 and version < 115: # NEW + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif version >= 115 and version < 120: # NEW PLUS!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 120 and version < 125: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN + elif version >= 125 and version < 130: # SUN PLUS + 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 + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS + elif game_code == "SDHJ": # Chn + if version < 110: # NEW + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif ( + 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 + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS 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 @@ -381,7 +396,7 @@ class ChuniServlet(BaseServlet): else: endpoint = endpoint - func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + func_to_find = "handle_" + self.strict_underscore(endpoint) + "_request" handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): @@ -419,3 +434,9 @@ class ChuniServlet(BaseServlet): ) return Response(crypt.encrypt(padded)) + + def strict_underscore(self, name: str) -> str: + # Insert underscores between *all* capital letters + name = re.sub(r"([A-Z])([A-Z])", r"\1_\2", name) + return inflection.underscore(name) + diff --git a/titles/chuni/verse.py b/titles/chuni/verse.py index 31138b6..6932bda 100644 --- a/titles/chuni/verse.py +++ b/titles/chuni/verse.py @@ -60,8 +60,8 @@ class ChuniVerse(ChuniLuminousPlus): "length": len(game_course_level_list), "gameCourseLevelList": game_course_level_list, } - - async def handle_get_game_uc_condition_api_request(self, data: Dict) -> Dict: + + async def handle_get_game_u_c_condition_api_request(self, data: Dict) -> Dict: unlock_challenges = await self.data.static.get_unlock_challenges(self.version) game_unlock_challenge_condition_list = [] @@ -84,8 +84,8 @@ class ChuniVerse(ChuniLuminousPlus): unlock_condition = conditions.get( unlock_challenge_id, { - "type": 3, # always unlocked - "conditionId": 0, + "type": MapAreaConditionType.ALWAYS_UNLOCKED.value, # always unlocked + "conditionId": -1, }, ) @@ -114,7 +114,7 @@ class ChuniVerse(ChuniLuminousPlus): "gameUnlockChallengeConditionList": game_unlock_challenge_condition_list, } - async def handle_get_user_uc_api_request(self, data: Dict) -> Dict: + async def handle_get_user_u_c_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_unlock_challenges = await self.data.item.get_unlock_challenges( @@ -167,7 +167,9 @@ class ChuniVerse(ChuniLuminousPlus): # try adding recommendations in order of: title → artist → genre for field in ("title", "artist", "genre"): - await self._add_recommendations(field, user_rec_music_set, music_info_list, rec_limit) + await self._add_recommendations( + field, user_rec_music_set, music_info_list, rec_limit + ) if len(user_rec_music_set) >= rec_limit: break @@ -220,9 +222,7 @@ class ChuniVerse(ChuniLuminousPlus): excluding music IDs already in the user's recent ratings and recommendations. """ # Collect all existing songId to exclude from recommendations - existing_music_ids = { - info["songId"] for info in music_info_list - } + existing_music_ids = {info["songId"] for info in music_info_list} for music_info in music_info_list: if len(user_rec_music_set) >= limit: From 064f2b6b540d6a5ec0f15a916e9f5076b96d97f8 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 21 Aug 2025 02:27:29 +0800 Subject: [PATCH 36/63] enhance music score path handling and improve error logging --- docs/game_specific_info.md | 12 ++++++++++++ titles/mai2/prism.py | 7 ++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index e05a771..eaa0165 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -283,6 +283,18 @@ crypto: "23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] ``` +| Option | Info | +|-----------------|------------------------------------------------------| +| `chart_deliver` | This option is used to delivery charts to the client | + +If you would like to use chart delivery, set this option to `True` and configure the directory to read from. Then put charts in your chart folder like this: +``` +chart_folder/23/music001736/001736_00.ma2 +chart_folder/23/music001736/001736_01.ma2 # PRiSM +chart_folder/24/music001901/001901_00.ma2 +chart_folder/24/music001901/001901_01.ma2 # PRiSM PLUS +``` + ## Hatsune Miku Project Diva ### SBZV diff --git a/titles/mai2/prism.py b/titles/mai2/prism.py index d5aae58..dbb00f6 100644 --- a/titles/mai2/prism.py +++ b/titles/mai2/prism.py @@ -34,6 +34,7 @@ class Mai2Prism(Mai2BuddiesPlus): padded_music_id = str(data["musicId"]).zfill(6) padded_level_id = str(data["level"]).zfill(2) + music_folder = f"music{padded_music_id}" if data["type"] == 0: target_filename = f"{padded_music_id}_{padded_level_id}.ma2" @@ -42,11 +43,11 @@ class Mai2Prism(Mai2BuddiesPlus): elif data["type"] == 2: target_filename = f"{padded_music_id}_{padded_level_id}_R.ma2" else: - self.logger.error("Valid MusicScore type!") + self.logger.error("Invalid MusicScore type!") return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}} - chart_path = os.path.join(self.game_config.charts.chart_folder, target_filename) + chart_path = os.path.join(self.game_config.charts.chart_folder, str(self.version), music_folder, target_filename) if os.path.isfile(chart_path): with open(chart_path, 'rb') as file: file_content = file.read() @@ -60,7 +61,7 @@ class Mai2Prism(Mai2BuddiesPlus): } } else: - self.logger.warning(f"Chart {target_filename} not found!") + self.logger.warning(f"Version {self.version} Chart {target_filename} not found!") return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}} From abe480d007f289e8b02efeb07c30beb2afc89ae5 Mon Sep 17 00:00:00 2001 From: ThatzOkay Date: Sat, 23 Aug 2025 20:35:58 +0200 Subject: [PATCH 37/63] remove duplicate method --- titles/ongeki/schema/static.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index bf4af07..f390959 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -604,13 +604,6 @@ class OngekiStaticData(BaseData): return None return result.fetchall() - async def get_opts(self) -> Optional[List[Row]]: - result = await self.execute(opts.select()) - - if result is None: - return None - return result.fetchall() - async def set_opt_enabled(self, opt_id: int, enabled: bool) -> bool: result = await self.execute(opts.update(opts.c.id == opt_id).values(isEnable=enabled)) From 415bbc92b38493463189677556eff59e13b2560b Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 31 Aug 2025 19:49:06 +0200 Subject: [PATCH 38/63] chuni: add userbox sub trophy, fix unlock challenge --- .../versions/49c295e89cd4_chunithm_verse.py | 4 +- titles/chuni/frontend.py | 6 +- titles/chuni/schema/item.py | 2 +- titles/chuni/schema/profile.py | 6 +- titles/chuni/templates/chuni_userbox.jinja | 60 +++++++++++++++++-- titles/chuni/verse.py | 14 ++--- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py index 61b813f..d52e445 100644 --- a/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py +++ b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py @@ -44,7 +44,7 @@ def upgrade(): mysql_charset="utf8mb4", ) - op.create_tablee( + op.create_table( "chuni_item_unlock_challenge", sa.Column("id", sa.Integer(), primary_key=True, nullable=False), sa.Column("version", sa.Integer(), nullable=False), @@ -59,7 +59,7 @@ def upgrade(): sa.Column("conditionType", sa.Integer()), sa.Column("score", sa.Integer()), sa.Column("life", sa.Integer()), - sa.Column("clearDate", sa.Integer()), + sa.Column("clearDate", sa.TIMESTAMP(), server_defaul=func.now()), sa.UniqueConstraint( "version", "user", diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index 1059d7c..e860400 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -655,14 +655,18 @@ class ChuniFrontend(FE_Base): form_data = await request.form() new_nameplate: str = form_data.get("nameplate") new_trophy: str = form_data.get("trophy") + new_trophy_sub_1: str = form_data.get("trophySub1") + new_trophy_sub_2: str = form_data.get("trophySub2") new_character: str = form_data.get("character") if not new_nameplate or \ not new_trophy or \ + not new_trophy_sub_1 or \ + not new_trophy_sub_2 or \ not new_character: return RedirectResponse("/game/chuni/userbox?e=4", 303) - if not await self.data.profile.update_userbox(usr_sesh.user_id, usr_sesh.chunithm_version, new_nameplate, new_trophy, new_character): + if not await self.data.profile.update_userbox(usr_sesh.user_id, usr_sesh.chunithm_version, new_nameplate, new_trophy, new_trophy_sub_1, new_trophy_sub_2, new_character): return RedirectResponse("/gate/?e=999", 303) return RedirectResponse("/game/chuni/userbox", 303) diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 84c6364..90b993f 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -293,7 +293,7 @@ unlock_challenge = Table( Column("conditionType", Integer), Column("score", Integer), Column("life", Integer), - Column("clearDate", TIMESTAMP), + Column("clearDate", TIMESTAMP, server_default=func.now()), UniqueConstraint( "version", "user", "unlockChallengeId", name="chuni_item_unlock_challenge_uk" ), diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 5f54f5e..c642514 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -463,12 +463,12 @@ class ChuniProfileData(BaseData): return False return True - async def update_userbox(self, user_id: int, version: int, new_nameplate: int, new_trophy: 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( nameplateId=new_nameplate, trophyId=new_trophy, - trophyIdSub1=new_trophySub1, - trophyIdSub2=new_trophySub2, + trophyIdSub1=new_trophy_sub_1, + trophyIdSub2=new_trophy_sub_2, charaIllustId=new_character ) result = await self.execute(sql) diff --git a/titles/chuni/templates/chuni_userbox.jinja b/titles/chuni/templates/chuni_userbox.jinja index 5114b17..3bca2fd 100644 --- a/titles/chuni/templates/chuni_userbox.jinja +++ b/titles/chuni/templates/chuni_userbox.jinja @@ -44,7 +44,25 @@ Nameplate:
Trophy:
- + {% for item in trophies.values() %} + + {% endfor %} + +
+ + Trophy Sub 1:
+ +
+ + Trophy Sub 2:
+ - + +
From f346d8572dc3eeabe8d2dc9374e78c2675bbe001 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 7 Oct 2025 13:26:54 -0400 Subject: [PATCH 53/63] chuni: fix typo in upgrade script --- core/data/alembic/versions/49c295e89cd4_chunithm_verse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py index bcfbbfe..91c614e 100644 --- a/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py +++ b/core/data/alembic/versions/49c295e89cd4_chunithm_verse.py @@ -1,7 +1,7 @@ """CHUNITHM VERSE support Revision ID: 49c295e89cd4 -Revises: f6007bbf057d +Revises: 7070a6fa8cdc Create Date: 2025-03-09 14:10:03.067328 """ @@ -13,7 +13,7 @@ from sqlalchemy.sql import func # revision identifiers, used by Alembic. revision = "49c295e89cd4" -down_revision = "f6007bbf057d" +down_revision = "7070a6fa8cdc" branch_labels = None depends_on = None @@ -60,7 +60,7 @@ def upgrade(): sa.Column("conditionType", sa.Integer()), sa.Column("score", sa.Integer()), sa.Column("life", sa.Integer()), - sa.Column("clearDate", sa.TIMESTAMP(), server_defaul=func.now()), + sa.Column("clearDate", sa.TIMESTAMP(), server_default=func.now()), sa.UniqueConstraint( "version", "user", From 77152bf25c0c20852add498373d85c95539e438c Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Tue, 7 Oct 2025 17:59:02 +0000 Subject: [PATCH 54/63] change down_revision when final merge --- .../versions/bdf710616ba4_mai2_add_prism_plus_support.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py index 3620b9d..30e736a 100644 --- a/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py +++ b/core/data/alembic/versions/bdf710616ba4_mai2_add_prism_plus_support.py @@ -11,8 +11,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'bdf710616ba4' -#TODO: change down_revision when final merge -down_revision = 'f6007bbf057d' + +down_revision = '49c295e89cd4' branch_labels = None depends_on = None From b1e629b3d720f9fdee0e9a579dd7222c182a57c7 Mon Sep 17 00:00:00 2001 From: daydensteve Date: Sat, 18 Oct 2025 15:25:23 +0000 Subject: [PATCH 55/63] [chuni] misc frontend improvements/fixes (i.e. webp instead of png; css error; hide subtrophies on old version) (#234) TL;DR avatar and userbox frontend pages can get hella slow when loading the first time when a ton of stuff is unlocked. Its driven primarily by all the images the server has to push to the client. To reduce the burden, these changes switch from using png to webp for all scaped images during import, reducing image sizes to roughly 20% of their png-equivalent. The filelist is long so here's a summary list of changes: - Replaced png assets with webp versions - Updated read.py to save assets as webp instead of png - Updated frontend.py and jinja to use webp instead of png - Added a conversion function ran by both the importer and the frontend on launch that looks for previously imported png files and converts them to webp. Only included for the sake of anyone who already did imports since the frontend improvements were introduced. - [bugfix] Fixed a css bug in the avatar jinja that affected Save/Reset button use on super narrow screens Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/234 Co-authored-by: daydensteve Co-committed-by: daydensteve --- titles/chuni/frontend.py | 24 ++++++++------ titles/chuni/img/avatar-common.png | Bin 35085 -> 0 bytes titles/chuni/img/avatar-common.webp | Bin 0 -> 7022 bytes titles/chuni/img/avatar-platform.png | Bin 25387 -> 0 bytes titles/chuni/img/avatar-platform.webp | Bin 0 -> 3602 bytes titles/chuni/img/character-bg.png | Bin 30878 -> 0 bytes titles/chuni/img/character-bg.webp | Bin 0 -> 1622 bytes titles/chuni/img/jacket/.gitignore | 2 +- titles/chuni/img/jacket/unknown.png | Bin 27489 -> 0 bytes titles/chuni/img/jacket/unknown.webp | Bin 0 -> 6558 bytes titles/chuni/img/rank/rank0.png | Bin 21473 -> 0 bytes titles/chuni/img/rank/rank0.webp | Bin 0 -> 2164 bytes titles/chuni/img/rank/rank1.png | Bin 33652 -> 0 bytes titles/chuni/img/rank/rank1.webp | Bin 0 -> 5004 bytes titles/chuni/img/rank/rank10.png | Bin 30558 -> 0 bytes titles/chuni/img/rank/rank10.webp | Bin 0 -> 3944 bytes titles/chuni/img/rank/rank11.png | Bin 25195 -> 0 bytes titles/chuni/img/rank/rank11.webp | Bin 0 -> 3670 bytes titles/chuni/img/rank/rank2.png | Bin 26546 -> 0 bytes titles/chuni/img/rank/rank2.webp | Bin 0 -> 2444 bytes titles/chuni/img/rank/rank3.png | Bin 30641 -> 0 bytes titles/chuni/img/rank/rank3.webp | Bin 0 -> 3766 bytes titles/chuni/img/rank/rank4.png | Bin 30641 -> 0 bytes titles/chuni/img/rank/rank4.webp | Bin 0 -> 3766 bytes titles/chuni/img/rank/rank5.png | Bin 28331 -> 0 bytes titles/chuni/img/rank/rank5.webp | Bin 0 -> 2542 bytes titles/chuni/img/rank/rank6.png | Bin 28331 -> 0 bytes titles/chuni/img/rank/rank6.webp | Bin 0 -> 2542 bytes titles/chuni/img/rank/rank7.png | Bin 42548 -> 0 bytes titles/chuni/img/rank/rank7.webp | Bin 0 -> 5100 bytes titles/chuni/img/rank/rank8.png | Bin 42548 -> 0 bytes titles/chuni/img/rank/rank8.webp | Bin 0 -> 5100 bytes titles/chuni/img/rank/rank9.png | Bin 21428 -> 0 bytes titles/chuni/img/rank/rank9.webp | Bin 0 -> 2376 bytes titles/chuni/img/rank/rating0.png | Bin 20220 -> 0 bytes titles/chuni/img/rank/rating0.webp | Bin 0 -> 570 bytes titles/chuni/img/rank/team3.png | Bin 45380 -> 0 bytes titles/chuni/img/rank/team3.webp | Bin 0 -> 4170 bytes titles/chuni/read.py | 36 ++++++++++++++++++--- titles/chuni/templates/chuni_avatar.jinja | 6 ++-- titles/chuni/templates/chuni_userbox.jinja | 15 +++++---- 41 files changed, 58 insertions(+), 25 deletions(-) delete mode 100644 titles/chuni/img/avatar-common.png create mode 100644 titles/chuni/img/avatar-common.webp delete mode 100644 titles/chuni/img/avatar-platform.png create mode 100644 titles/chuni/img/avatar-platform.webp delete mode 100644 titles/chuni/img/character-bg.png create mode 100644 titles/chuni/img/character-bg.webp delete mode 100644 titles/chuni/img/jacket/unknown.png create mode 100644 titles/chuni/img/jacket/unknown.webp delete mode 100644 titles/chuni/img/rank/rank0.png create mode 100644 titles/chuni/img/rank/rank0.webp delete mode 100644 titles/chuni/img/rank/rank1.png create mode 100644 titles/chuni/img/rank/rank1.webp delete mode 100644 titles/chuni/img/rank/rank10.png create mode 100644 titles/chuni/img/rank/rank10.webp delete mode 100644 titles/chuni/img/rank/rank11.png create mode 100644 titles/chuni/img/rank/rank11.webp delete mode 100644 titles/chuni/img/rank/rank2.png create mode 100644 titles/chuni/img/rank/rank2.webp delete mode 100644 titles/chuni/img/rank/rank3.png create mode 100644 titles/chuni/img/rank/rank3.webp delete mode 100644 titles/chuni/img/rank/rank4.png create mode 100644 titles/chuni/img/rank/rank4.webp delete mode 100644 titles/chuni/img/rank/rank5.png create mode 100644 titles/chuni/img/rank/rank5.webp delete mode 100644 titles/chuni/img/rank/rank6.png create mode 100644 titles/chuni/img/rank/rank6.webp delete mode 100644 titles/chuni/img/rank/rank7.png create mode 100644 titles/chuni/img/rank/rank7.webp delete mode 100644 titles/chuni/img/rank/rank8.png create mode 100644 titles/chuni/img/rank/rank8.webp delete mode 100644 titles/chuni/img/rank/rank9.png create mode 100644 titles/chuni/img/rank/rank9.webp delete mode 100644 titles/chuni/img/rank/rating0.png create mode 100644 titles/chuni/img/rank/rating0.webp delete mode 100644 titles/chuni/img/rank/team3.png create mode 100644 titles/chuni/img/rank/team3.webp diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index e860400..aef8c1a 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -13,6 +13,7 @@ from core.config import CoreConfig from .database import ChuniData from .config import ChuniConfig from .const import ChuniConstants, AvatarCategory, ItemKind +from .read import ChuniReader def pairwise(iterable): @@ -91,6 +92,9 @@ class ChuniFrontend(FE_Base): self.data = ChuniData(cfg, self.game_cfg) self.nav_name = "Chunithm" + # Convert any old assets created with a previous version of the importer + ChuniReader.ConvertOldAssets(self.logger) + def get_routes(self) -> List[Route]: return [ Route("/", self.render_GET, methods=['GET']), @@ -252,12 +256,12 @@ class ChuniFrontend(FE_Base): artist=music_chart.artist title=music_chart.title (jacket, ext) = path.splitext(music_chart.jacketPath) - jacket += ".png" + jacket += ".webp" else: difficultyNum=0 artist="unknown" title="musicid: " + str(record.musicId) - jacket = "unknown.png" + jacket = "unknown.webp" # Check if this song is a favorite so we can populate the add/remove button is_favorite = await self.data.item.is_favorite(user_id, version, record.musicId) @@ -313,12 +317,12 @@ class ChuniFrontend(FE_Base): title=song.title genre=song.genre (jacket, ext) = path.splitext(song.jacketPath) - jacket += ".png" + jacket += ".webp" else: artist="unknown" title="musicid: " + str(favorite.favId) genre="unknown" - jacket = "unknown.png" + jacket = "unknown.webp" # add a new collection for the genre if this is our first time seeing it if genre not in favorites_by_genre: @@ -370,7 +374,7 @@ class ChuniFrontend(FE_Base): item = dict() item["id"] = row["mapIconId"] item["name"] = row["name"] - item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png" + item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp" items[row["mapIconId"]] = item return (items, len(rows)) @@ -395,7 +399,7 @@ class ChuniFrontend(FE_Base): item = dict() item["id"] = row["voiceId"] item["name"] = row["name"] - item["imagePath"] = path.splitext(row["imagePath"])[0] + ".png" + item["imagePath"] = path.splitext(row["imagePath"])[0] + ".webp" items[row["voiceId"]] = item return (items, len(rows)) @@ -418,7 +422,7 @@ class ChuniFrontend(FE_Base): item = dict() item["id"] = row["nameplateId"] item["name"] = row["name"] - item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png" + item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp" items[row["nameplateId"]] = item return (items, len(rows)) @@ -464,7 +468,7 @@ class ChuniFrontend(FE_Base): item = dict() item["id"] = row["characterId"] item["name"] = row["name"] - item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".png" + item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".webp" items[row["characterId"]] = item return (items, len(rows)) @@ -482,8 +486,8 @@ class ChuniFrontend(FE_Base): item = dict() item["id"] = row["avatarAccessoryId"] item["name"] = row["name"] - item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png" - item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png" + item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp" + item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp" items[row["avatarAccessoryId"]] = item return (items, len(rows)) diff --git a/titles/chuni/img/avatar-common.png b/titles/chuni/img/avatar-common.png deleted file mode 100644 index 030b55ed969af5cf84e65e403340654b46d8a031..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35085 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 diff --git a/titles/chuni/img/avatar-common.webp b/titles/chuni/img/avatar-common.webp new file mode 100644 index 0000000000000000000000000000000000000000..1e2033fd50849db281b3f4ccbf914f7c75c3a390 GIT binary patch literal 7022 zcmc(kRZtu}zs47*NP$I*ZgD8??(SMBZpESa;$9pI6kA-2OL4cwU5hSSiWGM!u&3vJ zFV4C8Zol75CYdCY{}Y>eGSO0$ld~oR0Q6*~G~R0n=wko?0NlUn69NE^0FYGCP>{m} z01!|=J;BSh*5I4jsjUUBAEOsxFwl)R@2N5j2IF();qh#dP_l1KF)Zt-8*o088;ci! zU0R!i+3E!MZ!!hPJ8MoHXe20VK(uoRuP+;im!aT z6D)VNl(5fL?P_bx=0)r{MdgZybpF?S08xrthzwidH^mM*-w(mbRl>zFhKL?ktY7UT zkaYfL6%j}>9+5iyD$*!gd!>xF=dVjFB5`N-wN7f+lGHK-fV18K%-O=gx%V5=D4XP8`ONumi6_Q6f`}$;NLr*ErPgk6@LqN$7oXp zwLpG)<|8tPT*;KF?2QK4NUrSVCjZGNQGFvp@KG|;B3i78fxYr{!V zJa0QLXuVeB%C6t%7pCfyBu?hYy*Ag?6+l`ilMSFu%MJe#MoXDmZRnPLA*iWaIFDlX zP34|L(NU$B7;rAD$y=C?F@gznqJ0GsK=0d^(+@e5$CxprrjL%0Q4ew1V7L8qdX2tZ z3gk^3c$69Bm~aOd9UIO`OAs>{r~~yFw)ksBk&fiM|h0L`m@vx$@1 zC)G$$8PBk29)-HK}rK3yVoYHm>VdWhT@e($dhXsE*9P#TnSSC%?C|<5nSC zQtE);^In#oobwiII>a*ArKB}OQ(V&;;R$kQ%exII#$pF%v2u23ICw)HY&UF>r;6EH zD=oW9x~`$QQl25l(qxVe&+}}#w`ga;AlbK%`nWb9U7tU!lx>W zVemT|ER&Bm(|~GJx#JY{vEwD%U6BH?`A-S>nLMe#iDltQI>y2}bgoU`s8v`~Is`N& zIenJoC)UxF6+BJe;#g$$3tM^ELw>u;Y%#FSVbDyn6UCwUB$dHS2DE*=Wy;w+{PZl+ zKW5VTZfGjwPMkCRn_P+vo%hukKs4T)m0r`(6kug z>KGsUR6$-G%8k}xS0DKf#F#yGldL%VB1nlfh)kJxSVz(69ZqRRoUYdIR9HFP?rQ6T zh`~aR&JT?|AiOy#6~Gu;{q$fCR*FOnrDwIK zG~E`gQ1v%6-*m#D|1+{ zvC-clJt(rsY7)Ne2o?_`(=CzJ@$6N z-DoHK+bgUgoIWspx+SgEXO0zIWUv~&vAyIJPbOBPdKIl(Q8HVoDP{(g5Q|cl~G|^u9rLAFq3Rz*CXhr;1o-?ki zygZI&2f`xmSfc}a6%8l0B1ZUiK=#G+vpzFXVK$CW5-BY!@LSd&nR|EoQ9Mv-M04yP zN^}Udy-!7^bkq<7nH(t*UHJg-G0o?$LIl=IGV`Z6D_F`>Wu^hjd%VJ@SGswKK zx(LaV=TqoMVC#wnAG33~d0=mVTI$R+=@OmBI?Smvs}FH#53SN8D?hxZF)o%^@}j_b z$2=h)3!sn4IMVoTMd?m&CaqaOzr*9Hf`=;3P4z zKFR9O>oWYtfVkDIC4;&TPVmhHW9Z>@YYl~ebop|NuNA5a>@2DL{z28uKaT7=w!e#M z;#saWY6-<8@m!wNh(RE?+_Z;ry|(}7=I2=NCO>tl;noqZHfZL&hJS}M4PEF*kGS3w z-byJ(-p)U`X-B$@co^91Xd&*6tHE`{@34X}`)!0&Q=+C{&G!(5pxHJe>?-X34!?ki z;nP{`dN`$!IY@WOjBAoJ7!S9Als1VNK9NqJ~%+YUv1OlXC zkq2f@vbKuEi4tD;H2$SfLZIvn2m?oJdNrm#0{v9>L9Oc@tr7yBRna+U??q+>mW%F< zuT*Z20=#jT{5)Og(rocjV1C4maSF*Eh1Oo#k86vb;P%}O`4oTXYh@QUU3)4uytm{L z4Z0QbOLWZ>uFD&id{rr8;-@vlS*KS6~BzJKp$jhcGd*+ zm>cl+`78t|OQljnfM8tCdl!M~5!!51WZv%hucaf**=wWQ$k}!=jze!`UW;Wb@jfan zm?DdI$M>7isr9wRgA#@hYX+nRkkI=h-J5}%1y9tBmrg0fsZsgsGQi>dg-Vz30mox? z;D#v4x@Jf@<0eL?Q9PGU2nr*LOj118>uLN1ZX;#+!Y%q%l3Hz0!gnK$JzbJGeu6#Z z`h%Fc*G>dOM3U*cB!ad$iUS~Xa*Uw~o1$vTC@?)%!rS%OW-YhbcQ53 z{&4H6K47=pjI|*7pF}pP3@5yrp7*?)m4Eic(I8F=Y`^! ztysL5{?+3Il3^IZ=ZY&JI{Y=^tYvckj0me6o#Ry=kv4idUw<9iM_Qd=g(r;@O*ItX zAJo8Z<0VO6-gViw!qt-pd-{PXui?_Ea z)Bp!^1vUxjT`a-_M}Hp8^`1@KJ!h*4f4%AQMXbJP-@) zI>!Gg%sz`80xRd6s{fdqza^b%6(^2&PvKnd&1N`Lw?8-IO8t+oeNlf9{|^O}fWQ74 z^oRohxcC8b5SU#M0El8vJZXxw)CGlazY1vtV@KOKZ+mKzfvC3lQ2iOQx}5%O?m9c^ zAg!WtP`wJ!2#mNze%5(bc;Y@a{^NQ?^U!k-Up)h#-I#vg_$@x^8yr;da`Hp_kM1$T z|tDo+D1XAjtq`wMWyJgJu~MuvL0mPx*8e2^ks zusaYSRuR;BodX#tfT+(PU5gh?Ka1~)+&qJiDV}!!pgpr21)0IyHb#Q#Ej|7^|Nqab zF_Eb5zo_|y5Ehf}!uBB7E;1!m%ZjB)by57uW45;Hg#Y%?K_(b2rO8@(c{}dXOVAlW zsZK}g3h*jC{g;ktX*E>G12COFdh=y+g%|ytfljxEA@eOmWLvcRjxAmg^*_9gtzA=i z{B3#LV)`PXX!h>lseq_VeFxRf8l_fd;$OgJ&3TZmdOkX?R8PmdETFT@KS+0>;M$s4 z`gjD|S*^gQM?0B5Vw9!*;oMBs=$3p4jx8CE?f8f4WRtP%Q@)Z^SBzMcF^cD`_l$c5 z0Q}>TS)L4zz_r(rSpc~e^IbpUi>{i#>c=RRNxX0!^Hde%f?lwe)P!T%+w?46_`k>h zE9c;!1yJ(|C+`b*>X^7Z9~wpaTcuNZgfQ zYi_x@xw#H?eK1SHXe~xFh|IaGEGS86V&+ij*gMrFN~t!HY`I2-8omdSO^pgvykjUn zk&ZXM4#rd&mNnK#wiiunpRG$UeFx@|_1usQIQLBC8bxC;Dye z#~+hO;YU-0PoFGa_~5}tn=^>`y3sZZBK%Bs!-pGLrr(*lL)`MZ_GOJAHYmPmuNHG+ z?>BlJNV-5d-3f&Gd8kCpy~0eunwxx=oDa)A@yDh^S1o&Jeh8lA!a7kq-%Y}EarKdA z;zpgMWf9-qZ~PO9|L;tzU823Lbgc$iWFZP+jDI)a@3{Xq=0PL>SNnerHUQuS-s*xh z{Bju!hfnsRiBI%_`+lnki_lE?^Ixm=fyaIAWBa4NVhq=z+##R8|Ms98a3IW+Mvuik z{_`ar+o$P6kT=$T(cw}{|G*-@{=5CL4!b1*o85??s+aU%QzJ_w9$oc+t7YyVm?O#k zWbWe>F6X2)xtRa9M})jwjT!l=b6qeAn$M(~TI9vY{DOyqtvand;J4s2F%T7)zMr2( zlBX1B_QQfjcMLiwg#N;87Rn?$iHEyFunHf2ANl>=TxZ*1*ajlx8u7qAd9KLR)(gEta$3Howo?=-j+zSm5}>q2t%7O3f?$ z0S@DcAVKMe!i2q=OQo74MxOynpxzF)r5OC;MoXaY=*F!wjbhx?Glm#w8Uezi2ohCQcik@JRWb$UX5ZV_grR(epex zBHmFs@nN^u_>#MI9F?7t=2IrG(yL={M@aDAB~jnlJ^06-zMWz?Tc9WQZAD<<(ovV! zZl%ZmDLpFVa0p+;!IZ!Fey%nTxSyNIB()+nYfcLZvN%x7HoGN8kKf_U?KZnVJN`Hx z|77G9w&+`|e2UDFxlSgjbUX04R3?_D7w~wIZ-Z(ue?r{%emo{mgcugEd&h=nZSp-A znZYmj880mLgJL=l3wg~N-;kh6JhoWB+-SZMrA0M{1EZ)U&M2$aUAAzG?#C*p-ZjWK z$s%W*-%+~IdRfJlRZV}H;s@CsldjZXTUn19Y{+HB#ne(OtA5}GqO%@&PnEN>MLG&S zqSQT&`BUIN%X@F)E!2y0-v}Myrt&$w7R+au=r3^#LkBVU-_Oc=GSBZc?a!N0D4plc zTn)-~|4+=)F|RK*h?bF4Wj-On zTBGvDq*f(3ERQv-g+eNrDK2~xt%vwOK4&=L$&{=8y)@cpMTM1964yo6_}A(qU=0dL zW4d{J-q)gdq~B}XDU%CBI>xO%DI-UvJkMjOm6JnLY^b(0Vi+OKEHy!~pp@D^nFAlNWZFzaSi#{^q z?Mce^I>E)Viz{IVDh}nz+!*xKCHnf~XuGAbC+WuQE6_37tJf7wbEJG-zX-Z75Z9GU zY8S3kRTlO=w3ed_?J5#R$XYa}r+iG&we!U9jY2d<`|JXU9~*A$zbsbcYvkZQ*X1&< zkkcKP_=PGL8yk3YP24NzeN~uBBinyNNBc(leOtfGXt3pBVY{iCPFleOG)EtyvRYIQ z^X*2goaDo zblP3Pitz5TrZ%}4Oy7b>`^Ro-&A0b0Y>a@+alCh^@}U93o-S@oSg%JG+oG*W5!f3- z<3%At6&e3(&hd>5#9^u9G*HXW@`me^A2*X$g4k5O^YLuU8Eg+;+qV1i92fN=o^s;R zPW-^8R-P=ll92-x=_pGw_TI}taYMR`p5rWn7LmiLue2{4f~ha|sSCexo-x)jkB^ar zFwB5#Qz&$mKgLS1+#WD3xiy(w4rncVu z1`2ATXerb^iMserhA}SXHMl=~>8&shB6C*=ToS&{?QGhjqH-=}R$mzm)^VYuH%Ku@ zZsN0@`lNvfXH{Vkrb6H3Kcw^@x;8pp*_Pnss-d&<9}f-!4f0}iu)Yexpbw4tD6tMG z7aioh*zGw`ot|z@#q7f}j*RalNu;cqMVre%M_WErNrncL;fqrakB(l8x7%2`?Hs2= zPXWSUCI?DsMf)mhCBADV;IBs!=*!>tnijhU-+r->;$c%*ghn;C;yH6-JM}hLgRFz8 zK`Vf=>*6NI=t&AE{fk?z?eY#ztbWIs;AP`*x}CdcNxNiHNNfU50KS&c3ci(|+@ZYm zo0%nWQRU$EA5D9S2fpPeC>6cIZHh>5$uJ}UA*}sU!z6w5$sIT~y>&@MmQ-FR#mb@~ zTg?{70}{t=&}1g<1p1cTGU}W21V1OL#~r$V>l1fJ@W!Wl$e?~C{fZUK5pblF;WGX0 zgk>QX1C^GNcN!o~lcR!_luq^_++6|o62UdG-_>^8(@E_Tkw!WYi&2&UpzZwLP?o=r zWcfLnF8p+T?g#tMTe_0YV$bXz$!q3gSJO@tBV>%)-!;I8PvmG3p5;WlgAp0x#M#RX z_D+?6`VK+bTVHl}Zj>kRull{4ZQOY~9N_}j(FzI&rYS}AW26?>&4&7cXvfg`f_zK7 zglHlk)41=2NXGoeuk&V~fFHFj^o&kB1T39S0SYj2q_|W$O$C7Di{~3LPY-FrfPg*Q zK6mE=zV22p99q(x;!aK?w|D{zgM0UcBu(1x9!Ls+i&W%>$;A~b=xZjZg~%#m5k_Nb zxNPD9D_|N`z#Z2(TlCK{56^~xF}mTEvEL`j9#ew=9rAbBo;_BXS@%V8gP9`iIGYb; zxY!$ih8_@K1~~GcsAiNbcn4`0j4U1~WsiDMIR$PtyIh=I$>XtX;5ibpZ-E4UHy;^F zEq5rYXWd5BCryg*WODaW$e!2>7mSFT8z=WPi7efew$G;XV@_wzJuk*4kpom!#w4sjK+1tu1zjWe2 zrrg6W$aVBCL~-MbgdR;!&?x1J+45dNBzq{QZI-Tzoj8&Fu>vltF=Pg7-Q>T>BF1Xa zSUm5{lv}H&uUlg|JBc}=0GQ~9A(d5TMK06auSRCPUr|d%-nj3ZD_XVNpDN}p*Q%WV zDvTWnZ@swfabHtckip8!k@Y8i<2-fS--AQ;>-Py?x96Q?ZZ)A1i|hiF7Bd8Y7oF-O zHw^S9JDCUfvK=K+DrCIv{zpBI*E9W(Z%^PN-rDD`rx`DohUg4j4NXKwtoeSRous#^ zC0Ih$Z$}EW{DRMZK?d1-PH{jRZ4HmedpYk)OM4H)qSH$Tlv-et(_EEBN_w{Gavfkq zJKR{KemKXw#Nm$+-EM=pNUeoZaDT%?i0`^yoI&Q18bm}0)!034Lc(mb)jxYl;NpYQ z%mn`&MZssAmo`HUgbrBKXXoRJj))sFvo+o(eF{e>_s}OX<*r||;eBJmX-OqijMG!} zIgd%)I=LI%DbhaM^03k=!abEr{1zi!HM04nybmNKMq)*AD%O6;nAhrD_8A&JI;S?+ zO97Pj&G7G3F^(1_X~*>aA6fqSO8RDJ=4I}r6SB#!yXoN^J;*B-P~$4 z)Z;e&>^2h>?oMZCKagdd$uMwg`p_C=tf`u@Dmu1Jb%$EztOT>1?(MmkJmV11Kr?Xq zumL9c-KWzT=$FYu5=$nX*O+VR3&n&{Pv~6X4CrI@v@z7A3mhskC+ zoA9gPj4DU>Cm8*iP|rzFBRG|=nd+{jVv3L>vU70_9LNt3(@}_rJ4Zw+K=(?L1S~$1 QRutV?N;m%fOZz|m2NN1%Bme*a literal 0 HcmV?d00001 diff --git a/titles/chuni/img/avatar-platform.png b/titles/chuni/img/avatar-platform.png deleted file mode 100644 index e981414d7bd9e454077a251c08cb79dca76b5354..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25387 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 Y3YaN_ZXELp zT9iFxtxzP&7A1Pmv|sM)^4#xp-OuN}@8{+7(K!Ee&hMP_{cYcK&i^x=++(QC#V)`O zgTc6Tbu>)C=Lz~RD>L|4sWg-RfXz+E(gOzLSV#X|0ZYCBhr!s5NLcKiJq~0~vWElN zO;{I;6?Stc+mW1!Fqltgs;Rx1=`f%2Oz)Js{)yl-`eYMcR$&wMQ^7oOTg64#R_mP* zNq@#`T)jq9bEV*ev=fY{PX|Zw8cT9UvUjiyh-91&%RCYJXzqo7rc+z_%&R90h3zV@ zuTG}kY+$Kku1Jf639lob(11DYgk5bgkJN-&KrkLV z{ksaV4g?I+-Djr*yIu^ds$b1<1IEe;gN#pv?|`lJhv6P<-s}TAb{@u~HL*`=T%ut0 zYjJ?o`5Q_lTNE@ytXUCdQp1YvFUiNem_aVfaJIr51ZEX4%1{}8(Zmc zkR$EL%qhDYt9BK_(uO5@V4y?quDUh;(Y}BM`l`{tcL1}c4? znSNgRa!ZKxfe`JLv(IZDPwl?Ep&_2_m`(Sq812u+9A7?fY!SVyYg1w&&eLza_V8(q zYyM*UvQKi3QKb(9yY8s6!aoj?dw{_BL4Vt73(u;TGcedh8M(T3 zJ1b+bL)eQ3?=SCE=e5(sV8M2}@rPlsgBn|;%pToTz0V4RX{3dSUsn^Hs)kEeF>k15 z>aOPajFUa4zU6L>`f7D{+u#lExE)tR)wf4HxxP^xhiu)o5nW~M5Ru@kLWJYSpRol57ctI;!YZ&MP#5>*iM(%T7)c z8mQhY*M?|TvB|FVRu{$#sp(kkwlM6_$rawP1-+(|O^%K6m~qWE(S%(6TU$%le7p;{ zkwzW2(2?BSwl-!Ra<%^n>|MDnYfoaOx0S>)=u)%Hn%6dKHM^Q@9+u2YLF))?PI%a9 z>vV%f;f$oP#DnmG)PdRonE}B8kqK+|RGVFgvt~_SZZ~=4sJDJ_^&onXnRrfGZEt3> zNl$LN*&0lYl{zK8$mE*w@!WOjBgqO+!uWLcFHT+ zMdC+!;~ue})jPZOte`m3EH6Jhzc-&_@8sS(vzWYtrU3<7du2@f^VnJwuk6iF-7{h; zXlkFgGxvDDeJ|#NRZ%3`{{AifE;JWf z=2qsdB3tzh-WZG}n;4X2@5tV{d*|-^PaZ$Xc+z$rlcJ1jmwuh{I;B0OuNY_LVZwS-aP7D;upg2726b=TS6Q>`kt} zFs2%CZz^#ZX>ZZ6x}ig+zi3_Ej=Q8()07r%dw%;8kFd!Tr%vjJ)=4F)NTmqUdLC2^ z=y^^bKJdb?c{(-aW=hRyZr8Oj*)h`C@$O5H(U*2yw3P8^tbKreZamne%5mu|0Zl2kL9Y09~9Ra)sinB z3h%=PZT@2W#qUcKEGJlOg+1fR5Va6%hPhe4qat@st15O8KiqgWsuq=2qQ@>S? z&BhvgQDl>B(}@o3esLkg2_10*zxcOtB_hc?h4wf9-mKR&Tn`?JZisD7SWG`VC zX=TL|ynM5rc#x`#1Kl?7Zr(62osS+;cruj1)S{^3dsK>{TX91bdNs_K=y? z_I7cW8}`I~c6Xkfe58l-Ir7lSh3a5z$xTl^`p@0!WB*<9DCUZ!vjXT z+eZ%=Ss*R@3h|#JQUz6bPevB-bnz$g26b%j_8AmBPJVRfeD|4Kc~|b1*f!WT+YP_! zMO?JzPRvhwmBj7J^{n$2SD*Q<(diGGUC5*-+q=rbim)Y_ca(0I6LTKcKiR2jmSr|& z_Q_0kthuKU-#+WwG$!a@{sH~=X_s~R%)H9O%C5aD_8L63drF+sox3@sT5vLdcs6m) zGH#RGqnZ&e7jA(P;-&DDf`pz4{CCjxp`U36cW;YGr zf#Dgpz8N7d#ZIp%|En)pS~huYwBLwyR8O8r)=Du=p0l>GE;V|r($w~**YC4-*QQk$ z&`OrRdXr`kNu9%?EeZA$4N<9QQv(8}K6c}}GK&0P6YBdMSCQY9uJ(Mz(C0 z>-SIG_MEuUHMnly?S0AD6c4$)@qDASkkDduzwAO;azV*{r;;(hib8+8nPI_;a`in^ z6~5zqU)r8tR8a1znDish+?$#1u2HpT*q-B0Rjn7g zJKl8%IaYb6O*VCF&oGCFH7vwU$E?CK#hj0+i{OeFxhSuqpzu=FeSY|J4z|IzVe3Wy zg^Y!|0)9SZ;Ox_p_WGuCP3cJ!NeOpcj$WRBQ#G>wS{JQ`CRF**M+S9p%6x9L+Pm9( zkO9~U=3L;)jJ^>M=!oQR&nHbi@ptFq|3vOA)}BwJ=k{4}v?%&67lS6{gF@+mMfVDvOHJ2m1N*ePJ$cKL>L4_zoU`5$S_>L!zW0 zh%_O3kdL?%h?-tRSI^CVHVF?;6XGwB z{s~c_`JW1M`>`ka^LhQO{!2J)2|s(f9dUR5swrCnlIToy0h9Lt{ZM}jh9}AHFBU+r z`OD#mKBT|sMz8tS?W;|FQ?(MXWJRny5${QMHzSjsRleElubr6Y_jw9Sqop9>?fXcs zwq$RQ9ZJZ>9efq)=Lqoq;u1hlOkp7>*kKN0f} zL+Ow_fKB=SW^2IWz6LT=;^99&{ZZje`Wlej@a`T&x}PhH{&4i45$tDxqWAe4FBI_v zdMHyN(2baAtAzZ+(_hp2+4Flh(m$&6pM&6={NK9&^%&j`MAxOv|C^|>$*?;W3cn~EdJpnkA{s+|If%p2G%CM6qpk#q5+Sy^y2na$G5H`|8 z-~j|Vj4i=VTGo~z`$O6PE0poeq6rRoS9_wZ3i8MLemwY3(x0px1|uhnCL)Ls3XPC~ zKx~w^LtzjoJ6Svt!YiQgXpjK@o-<)&6fx32vcUgqXZrWjpDody^nc@lAn4)&cO|-e zs1V4mUPN~gONH&+$u7bq7rZ?Y>E>!L>`4~JySX`&2zYwLMS8i~ey4;#6V(re{wD1| z$!3WdlmbCU28F;&%b`G;Mav@;&>#^?OQRq=0)&x~mjh|`Z`kZN=7Rajf0k{QPWgYN z@V}ii>`)L9ur6B!%2q)dVJidl2RNA=;4rrGAZ5tg%F=1l-_xHAqzK6`$DD6;Y7qtd z5jcKe{GFOD4gtR+xpTfe~NSe1BlP%wK<= zvi`=hf5PDgMSie_Yl4v!QiS_do0?ze@*~GfUsr5Qy>+M36--eOvQws=o>Q zv$4p3_rm7eD1Rd6*JwZV@EhE}T?i=sJls>nlAX!!24q{Jii{HS&mR9K^H()^mvhgz zS21}=ZT9=oal(Ej1s;`SEwi!bcjIrxN;wI8^7{oZMD&7bzP{L7xd)qL;yO~JptF7eg0zC8smV}KVykU!r9 z`3I}}NfG~*fBag~zf$TVS<8bg0%YmpGF(gJv#53%u0?<>U0jB1X?zydF2l75kfn>u za4n6`qS|G+76Gz!aT%_q@mW;64A&w+mM$*CwKP79YM0?!1jy3GWw@5cXHo4kT#Eo% zy0{G2()cW@U50BBAWIjQ;aVD>MYYRtEdpfe;xb%IEbe6OXIVsb{VckfGk~HhHGhj z7S%4pwFr=Fc?2MDertM47NdDS3}Lrr}J&Pv#f5f=Y26fgKBO?+UV(z=iZ>Sk`X9MjX z+OD;`X?bOQVL_;E2FP$8vIVp!2(HJQZCqy}i9PeihtFbz;X> ztMA>52LjvM_Z8DV9_0pu?7!B&TAkI%$cX;D$sx;ecTNlFX%*cv%p;p(a0LbDtZ^^l ztbK9)X~(;n7oLjYb%Se}!_?00-nucme;f|l%vp0Xhh4X#)YR4KYxnyoMT|@f&|4Vh z;FW}m1&-H>I2U)BXBLdm+e}V+TxmiZ$fp|S(*p0TD(fkD>Ju2qedVcm>yaMER)dyX zLC*&VFHdUr_Vr;~(4%1ipmy?O`8=wi-HAQ}H~CuvwJ)Ij@reog`n5Aj$!rKS`oY?JT=@-iOs}8>q7{)Wq{gs~+{f{Xsa`^oj^&k=5yPIH3p?9$^YiC7OyN{j zRcl>~IbKg%KK1eUcM3up5XyQYii?Xsd|4lJA1{6WoWPX((W6I?jd}@!33Yure$eF8 z>!T{gcYOEm9et;UygxVR-{D%Eo~^oTQ|LlfSC@5%v+QUXB|m>M?BKO_psmhLQ^Uh~ z^C*DGoCqB)97!XdIeO(MwGvuOi|J0A;w~3D%8JP2S!#XZF&(2`UF)Ymr{6^nl{LS9 zE&$dZuw94_b#gZ8j{bpmJk{dX9R*Tjotgl;rI}pMQU@@#kV+aQ8-mF;?$#3kss@&l zvZ8puR9D}Hg^MvUF@4Af4(KU8Cub%%Np9LfCX+XE3G6s}S19IwF5NJJ83;5qG-NPKu^qjYP6LJoW-6B{ zbOVNK%L+18Yy1@mbWu=VSZlutykHH^jPLg3T`Lw8XF-MrQ)XCW0^Nma}TRr zA}4OGUncVx-V&#)?H0PJ&$z#}R$H=u z!>xsJqGocXP3A$XWH*6(#;TVZthw@XpQ2|^3{C@2FR?VAeY@G4KPn^mnX+5TJ;;YtL0 z+Zh(e{AEMA@a2Y+)|QoM-AiFkt?0t{p`HM*wt{SuYtfC*)$z=HM$}MGw7`*iy{u3k zOqR=bzJzk&0AJsd9gc@vWA68-Ts|4Julf1rw&v$udnysHHAh{GN22@`Z&p8J!T1>0 zVH|cjcHwoy67hZtxzMYdM)M7-Up(7}fjL!fuwXUcd*mk9WlSRaR)%>q@La@J--muw?XXGx}0P7k?D?gSQ&a7W7?+>nk`x93S#T_*u%QP2`! z9I8plO8+QxF@w*@(zv#tf-~0ddpco`27?axN$d~vmjI?BBk7`VAb{b^I@;ReDKu%` z$X`7QksNv>vqXHAhH^7hYa8@zgjOMSK8nB*H-Y7<9)Cq?lumg`Nn=iJrIqtaIyPCa z2QjerSsNpVJE3=7gFV}9JUbF3?)ZWnkt8E!{kYLzL$`B=`#Vhn-f;&;K+Z`aVNLLMJ7g)GkHIeQGG@JFX#6(i@qR z8x1=V1tz4ruH`y?!n%y7tdy#12BWXFpc68RYm($79S=+B3k*@-hqg|bD+x63kZ?pB zc=S_zBCQIH^_lX#WP>#?3^X_h1eEg*d$@5Spko5J4g1eWo3&0PF&3x8KZua;P1rZ` zkMk5Js@7s`z`P!$gox~TyO}9s*2`kj+(wf!(D3|(SoFE#noW@d6bk{LG;M*~8xysh zC84|8%FIV{h3{b@y3rN_)lD-TGQXZH7Q@1b-CxVe*%Y!Hw2HxF$eM~G(M%CpYfkAW z@O;qa;?%`oI#(R3pO7854z3P_i}Gg8`lo@9!g&4YJ3D|yYCQ>vJ5#k54~4CaUcrQS$B_{-NS3Lzp>_HL~$!8aMO-PUp3*TJg!;5Es$?3{SxoheTO zk*M~O!g;khABn8$;&h*2UJlMdKZ)e~nM@H|$V|0H=&*6%NTHU!>?DicNFKNz+Z`@F^UfZ_VWcSJLln`Y?@dI`=tw2tNS7*LecHkbQKkwh(ZG= zXYZAty-*c9aO(Z#9qra9N!?=5nKu+SW?;9~*JJKd-Y3`6t-%V%TB{Pkh&Auf2EoYO zyt%68whoT9XO9t1XTpB=L}8>=O!3R7&ymOPTkEoM;`bcavev_~V#e0PBR|ThZiTi` zktPqajd0uBt>ZqQw*Xb>7%)bY(nY%JOm|}^=xckD<$_~1(ou*?6@-gMtAvO=v97L) z9i-@V>piw^`n+CJM=VW{5gSm!JEb`*Wy)BJAcyl8c3(Ql9Pxfui4XoLoGKRE$ZcL~iAp;+?r9 zk!*w(Z{4O@SGY9?XS!;_M)-}-=S@OHnjeHym3#FL!3DW?ZJ1VoWFrE~d-Woj(-GbA z&+8)L@Z=|U`Ng5ag|8@Qf~kZMk%!iCk)7IA9Y;wMH65tf(p6Cu7or$2rcJgXw}&5L z9Hq-maL!LIfu&UAg!!`It<@Rk45g-wSiVM=>JEBZ4&sQ44oM$$f2O;3qE;RQnF-M9 z$IVT4IbJJX8&-p%D}|3|Y}{Njh{i#f6f>KkJ6zJ%EnBI%qbnoEmI2A+J@7W0I z_@31m2QNo3sVIZYQSvdWSdi9B^GZhA2Qa`2p!2dG*RG^6X4`Gh)Vn)hdRIm$2~TI@ zo!PRAaBnO5dwSQ=w758Er*IHeyNN3W9zYo`aUCt}TXlUYX~CbEf!*@zY-2 z6lRXNen3wU-rT{@$vNmr-p)mEO6AvE88Iu{x(*&0(i&f!@loa=d}rH=8V-V~u^04j(2gcNAqs50!Ft8+7*&_muq` zK=#KqZ`HX2Lu$}0o(ZOkk;V8v>|i3yaB*7p>g83%t|UyJ$8vCv@69|-MJhmEY`~85 z^s73kRePkOMu&T2*$LJm=_-(y7cLhp_(<>&$F~PX+3CI#LeXm>FGt*oP3(S**p4f) zm(DhN46lsPtLlKm-_RS1kOY$%3Hl)-Mz6!;p(6Af3ILt%J3|fcx8Utb6I0G74*3VR+qO8M6 zc-MqIP^biyRo!8QJ5jXIMAs?qt1cpLaT$bD54R=-Q^BAh?-^fWAOORiiJ*#cS@mXw zh}2nEdvNEK$%1&~dSgFa5cA1T!l(43Y5Ca=Tsz*@+Ha!dYH|hAo|fPrfHm~$iAN!* zNEBSKxb@;GS26fb;7aVYS7B@zV3Y$5?_F{S9*L%jk+!fzNW!fr>`fT4)cTJ2Vw)r6 z_nz$w+;D65jq_XJJ3-3H_~0ggjPA=HWl&^I&=l|dj#yGIe2?=2=x7a_I6Th%^@vCvMh=&{0WFO(_b`Jl$w$j+&2 z{mj3r0~PIG-Qi>DYKU{7S`2|aYn~N4wFWAhxzT-Qj4Qr4{t?AND25NNx1w~erh{7N zerjce5Qr=byZgpE=^6=oB?J|Ls9MfT$-Q@TZJ>&#KvUHoL+?>wV4PVW{3L=HqYx!G znu#R=bfXZ1G-O!32b>y+C3$c4^?>Tr`Jz(aP%KiZ9(ZThWM91V!~Q14UBEv*7_e&- zEpz-NP-$~HH65=f6|T0Aq=cfGvf9RY^_$)^^Z;x((1>%M@{xph z^*3aB(z1y~tD>SS2xxfMI`~V0LbF(_5Rs7^-DdDE4o@(T9S11Nz3fsF0XXM_yR&P6 z!{~)jHwnO-YgEi}4<_sb*`s>y)3Xg_tkbjTm_TyzOEykpd^cbQ?(19`53ceNatE5kXHbcfc_R~H{5GJFulMl+<# za%PSIxdX&Dv`MKG(76%a#a(!_V&2e|-px(GU;XMnXhKlFLFXnmxY(zLVM0 zAOH4DeaDWT!gp){Fqhq#3&W6CWbwqND3{s}6Wj@>#P^wXmDVfZ~h(+6&2D`G+J}~se90Mt6 zb>E|AkA49;x+ywrITJy>jEgMfI_t_(6C3^XK-rXLF`o{|q^4*Pt9q+q+bk>UL1JHs zGT*J&`^X5SUh8LfHqu@l;V7z_c4$p`vemD4b^QjP%Gj0z*6ec@P*Dz}hEx>gNINh9 z{a*H0dLWJQb8t>qNbZ}kzk14b6YwPgYQ_g4NY+oH+b7}Z8Y{cGs*X#SE;-IH7lfcp zm1dqf%YF<=-vGCsw7;HvR$aS1SCcHpPct01zp8aCtBIdRb8+AXSv^Lhlc8bIjZ0{pErrEC4pR}>#)oxyOy0R~#zQBfgyr)zQ0K}dkQ0|ZsF7}Jmz zS(AhFa!rR2NT7+4RwEhK95h2NkgG%itA$%D=G_Ba8y3wRp$CjIX;80{^96B* z42(ji-Z-t5sTz{*U)zx@64IL0#6ru)w1P}2T*wQb@R6`m`_uy!Wmj)wZe|rZ*4eVY zj0dEef*z0=tsivPcPKy~GItW3LPR)dv<$DC&u%qbWWesv1P5Mnbn0aNCJGVKN(D40 zuOw5Px6s^s6dpA?ZZ7n6y&zUQzF0IoKnVgteP}S20SnIU1mP2I5<>8ug=_GSFU7H& za19xT&`r}6Syx!hXaU7nseJ^~TJm*n&9epd9U%AL*09>;{jk0RxHl(f zoR%cy=~3J0SSZd-@R#Zipi_tUF54YJJdUmgE-VynfqPIY*$zBA(kfYDHtLWyBpY-} zIYeXwHJ+I{;&!nKJFQKT9-hI3q_-*b6wZ0k648xd(`qqcz9lVXVW;%t$&X4x@c8W8 z@nAE2colr-tA?zq4)c8@AU+nEnk2n5#OZ{l71p^^s#e!@9ChheU?-7Y zR-Ry?jddLWssPEu7kxFO&BJoc0nHAMPT2_rYksz&gB0^1yL(UU5|{|DeE^^-{IrYp z9RfNCUx|1v@hA#uYB=fuJ1x=9sj@tsnTtA$?UF3VAR16?M@JAt3Ob|08mNoJo12(K$xb>)I69YC67{74gJz(VNCXg4{!Z2Wg z_PDYZq~IxcvYMTHBp_Q~iC(~NYdeN0cS9AeaJf7{ky$kzdAsDebS|>Juoufh0akiP z7%uQIgi58Kvwb?=8@Ua#4L_|ElHPjCm7a|n5lly}Hpa2g>hXuJR_*bxjXiE17d&>Z z_~nXHkftS|W3M?%Ag`;8$K;^990bm})x|cvS*l|j7abH}=m=W)sOeq-!r;J@m*a>^U+DIVB;NP01m93(uGb+v* zWNcRY-e}rhiN7Zs)-#aZA0W1NZf#30ut>33feLWmQL)P#xH*Vg!L5J;Pv6+jmPM=Q zuz-UcHGHERM9db4opX9P3T%8p^t;4On8ZR> z`Gmuo;!#~dZB-pY@7?P1wJG4sv~} z09>32Z27AD8*rnwjM(;(y$HvVJG?Z*63OC~QPI(?^f0-TpvpkYuD)dq+w}%Ux1LC=%6?yPzEHw@43P9aJLu$iqN>AWv-;cOcm{&MA|dG*wJ;+Q z4cPjWc~KYT89i4}4kTKGWLI<}08rK7F*pc4cX4kojCD?R&!!axyUKugQV8NnUalrQ zrO#w)J6zBnau(GIr!o@~wAiB%yoJr-Wny26_J!mG1}r%3?h%Y|$G>eI>jZf$v79## z>kZgG{n+Q8xT7-U#okC^itsT`0r`t3)ZEyA6(I9-cWpw!3`~!`0O>Jn4F{*LegZE% zo_?mlYGir8pSlqq=__%uDi-kE5D{b)1tTiS3m1R7ekU(YY9l-zeOnbAwsn3Q&Z+Il z6Ho|Auax8TWu$K$O^hX?KY@$@4s$|84z{*|eHq|fWIE)nI&7SzRDQtQrhEWvbAn`% xQNRpkMW^;BI{8W{F&W9Ie3DUurh*t@2g`Ym1kkiz(f^w_T}?xc0(D&I{{dr>z** ziD@%|Vf`UK3~;jm!~)0@;2*#MaGUzqbYGPY`S^o1-1HiBU z{)aQH_Kb@^{otFJ@$T9rR z-I`l_isT=(jj}-2z-DL=iDZF_oBP|<4cl@V0}RQ#r!81RZUrGU?8Us5g+5Iuqwf*I zK&7=y>##IcR&w~LsqM>tY}XQhSaDsalb$4~AV zG}vyn++OV1PjH%B@w8?K{0eG%l)FDfg_FHztS4mL$Q|CBCfc9nfB8!2Pm5e-t%Du@ zCXEF{rB3H&%|XFnIWkIYD-e5le%d5OSkq?wmaK^Hi4#R2c z7mWKW0;IDkESHk#t1m~7XBmDclvj!*GgH1^Y|{V${{232o(hXpFe44G>4i51smI!s zj;n*FdI;c0wEd+e;89%}<@8tZi8eYRcpHqB9(m;Hx8AnOq$_UnMrLe%Qxb zJM&pnpzJ_OrnFd6wxeZ?piYuZ{1mw9S(;;-q>+xKgPiBI6VT(fNy6P_6BMt4UB8%w z4V2TyB=kkp{IlvycW7ATz$7c3DumtigS_saA%}*vwcDnoTUtKxj-r|K$ry>t%!OZ1E`3d1(^Y@&3X5g(u zz`_ea=)x9C!Mg}w#x4!Mnp+>RBRk^$qwN>mrsI2@VBqMiNuzi=Ob@ara{1sae*e+~ z32yfV3!%Y#803 zj+8FXG5`LDOW`iIhH;9ku}uo2-}NisI6rWU96%w9*m0CE5Wk_7{B?CfGNpEW{OtBg z;n!O@Zh=`2Ja85YgakEJ8u1zQCx-QOAZPZG{he3=zCjN{B%y)Yp>bO-1^%^y?rW@q z#3Q#v)%B_=LN+8PJBLCRV|$WpK)l07p+{@XUjYt9uOfsi*60X2eh6(9?1az zp|wYdvEnGBbwPI52z`bi2Su&y6uEC=1ZM9!{%0$Tp47JV+Uw;EC)7<)2DIx0eb>b+nd-A_49T+$T?-5GX6X)p_y+jr*S)8}l(I zAUNLV$rHvW%5UH3`R5$w2IV}T&b;m@Dh=AT|H%Jk_^lkRjTZiJC^mXnxOVS1c1*8D zDnHRAVS8bFg5were)KP_NiMPdw&droZM}$!&Cnm%qaTIv@fle#!diA2|3oMd&{=)6 zZz&zWRa_n~K5o9~yclgW?TaeDpKiX(+mZ6&)y$P(lUV{qrjTMVCSo&vb`NKvU0eBV zql2#0#?nm^?;ak+6^}C|PrQb$DAwIhKG4>n+7j}}gRR)qxSm1$t--c$fWnf+eL}pc z2uVKNSb2Wp2quB8^fmgOC4h|nDd86Xm`#TJyV+1wP!{kz=vLw(Z=3S$$Q%?z^e`Aj z{!dX8tMsQ<5j(w@mW=4MtraL=2Km{#0X#Wa+3u;YwJE__EA7;4KWuCZ4`1x91 z1w?Kn0pZIBTM1)fF!)D$nXsVFsOi@2-iQfgP?n}Z)$I8fi&Y!Rm9rodU~rrp)%D>W zF|vyJu?^SMOR6(gm%iL7T}A1yJN}0ozE!Br)`75 z&Ft62_aY=c7bNeh`a<~Dec~9lGI4Em)a-D?0=H5S>n1rCYDy^UdG#8sxv|pU*mx$s z0tzyLLi-!FmVyxPWXk^O#9++pPd-M{N4k5i$ov0`rFHATg7&|1(U$+W^8b-yZc_R` zBO)2~e_Hzgm)P*IVl2eKU?h=y``Gq)_sZV0vJap1(8K?oMi$XN3~AV7)c?*FN~`54 zHhHgpY(HEYfqcWWJ^c7+qssM3YM0Mc0#+9wdq3wjk0};7f0% z&3*1#HrWam0-Z`_>v^_Yh8_4`0tm|s{OGoG*i5iw1Iq3BO#N&zYE5G(DAX+hSrtol zug}w_W!5Y1t=7x`w#*VLz1Opg3)EYBDub)~vgrx*f*58~?Vx&5rTKDasr9IbSYTbw zq${T$d#C9m_ezf5W~NYBN_Stg*S`NKvvaG_DuTZyv+}uR5cCT=68F!GRpwK2Uf>S2 z5qH};NQMVZ0K?aBF^d1ItLCd`Ob|_(TVf;ykSM;-yq>T^!Ojzwp zu{=T9`Q?%h+I?4gt0yrrs|qxOzWMeo_cR{ii`6lh7nI^QmuCasIBsxRjyGqvn9zIq zuCL}6>9lo;27ME4zEQfy3{UkQ+er&!338=${wLciD1N}^6~U)(y>aYM+~|7GdKv~9 z3rSzeLWKN27M>&KzH<@(2twitR@k=;8WIn3VVt>cJ12X9o|D!HyveCJFT@5#jLcRm za@Fxx*4_}VD_$*C2V+bi!)s+K`Q~dexVenJJu?uc{u%QuZ#O<}7`-~^PeV-4EPs4r zWhf8L*X7aK>7);|l0a75Bi9D3^h#Oc7&;cY=Cms2u#Wp;5_^^*Vo zOan#+;#ouj)t~}+_@xKPK32w7NM&R~wNwwUAPo9`;!UFY_9EHU;aZdX8-C*2=OENX zLxkiQI`N!<-7?ugj)i605E9~Hr&Zg=T!)OCy24^LGBKUM}M0GUh6i4@^ zfUJYe=OaaY5YHJb0M_?CI!a)Z5cj%^}Y+gMvL$Dgj?b1k^ z8Xkzs8RA8akc`-!Q$<3)|6COoA}vpZsM2#W z{>$;(0QB-Wr$4$^+-+lxUb%2b3MN{XXmhQ&J#i&{B3vwv(xMSr#L;GfH zxLk7*(e>*NIpmekz4RF0`eG~MivIqOAI?9^=QTC0-&L<~@KaKUTvhFAL5{S)^L$D@ z(qolc(BRw<&GQ(<6)&_srUP2kI-B$l62-go_5RPt!Rh?}WI?*du~i+1{(-)K;QRmJ z>HkaocpNba#hqQ++}1$e`+tQ{Y>H{xg|Wr{M@Qh@=;71H7^Bp|`!~H3zU+t7-g7qm zh;WZJx%Eod6b1bQ$Jl<)26B=m4*$W@N7W+QX2FLu`d|7>5V)}QmO#QRGA044spwc59MaXdXiQ6h$tp6w$kICo5jg99Ch%4?#{ei68V`nPNOAk#6%hZDy zz2G@ETD2O{Dd!VYOl+1_F1GPm#j=$yK86M|CMddf=1LDAR_m43z*DFWCXDCM1N`AM zVlc1u=|KHKp2Bky(@6JM6knT6YgDit2%-nStS)*ZArrm^Y1iVKOO7;#yS~< z2(^hd;3fhx4Zg6n?HW{hZL?=}Z=hz);e}Me@ujskR#rAx4f)(ex!Wpql{4rq{qS&f zRaV)P^xcPO`4fTP9A~E>2D7W>FI+jpjNGc~4MuJUR>lI7O%7JMh2;^$mqyrc0a2~z z=HlrI!vI%F(G)ZsRZ9oCy;f5g6Z9R9QFpN)3j>fA+U>s2vhrezNT&$~JbVYWI$Vg# zbw_TV>L2!e&A`d-{gbz2`dQjk;2Li>t&_JwnhTVOt}O?t*;zk{+A`~mmz3X*-$p4Z zdZbZ8v{2Tkeh&@ojE{g?x^D2a=H0~Ev!d#nw>t>*x~r#L7Pe5P+p2YqV82K{Qj_F( z>C5LZxuf|n$}T)oPCBwr}|$DjF`kYO$IUz{FqA6PGH z$-ku$P|#@l@R6MB2bkQ2+7eH?jRk6PcYzJdkgWOQ*K3I5vsJtP{Xk*$;O_8?w5e-Y zDgbu8h1cz7*=jwZFkKmHO19=>=9-5M6C+)C_aqKMe-om%a@e% zd2dJ?BNm*Ry>uXjl0{N^O&E6ZC(?1)+Z9$W;X6~^-rGxQWL*@?BePY@!|MKPRx(!i zfnqo`EKhqo2S5KfRR~RevTnW_6W4j}L(xTV?N2n#$PSUL)fx&kt$S@j95UUekS0skDR6N2Vo?G~{+uO?DOB5buQbDhMn(a=cA*+>EzX z`jd|W7`;BZhYp-@n4?22)MAai%yFY~XkE+;iy?IIZ~5yve;p`vquCt$aCJ^p_p@Wx&wRgB#Qu13Rn3Qm7rSblrqR*#T>JHoDW- z&+1ce`F@Jzt)-QlR^jfoB=orjv}RX&&%6w1;ycw3g4(Q^ss)|$QVcip90HH*p*NLK zD~4CcTp;P7_|zsZhn)`W8OLUx<+J_<@9jNbd~POmU^FEUmZ=z+d`FFfJLh}TG6AYv zYzgi+YB&&R(6`2=M?cUxJLn4lT7?f)h@G071nnRVjz7N z+tzVPxxL%Z(gT7O!Gr7bicF}58V43#v!JWX=^3RTlGTX>3(oahgpHeM6#IoTorcjz zmoD>+2glo7OK_B6Yp1BeTd0$!lhOn8P309bRMO{!?25!pnESefUBzvqwATR6^;0^C z&Yinx>&V{4w&j>i&}FYJQu{4*g{^3*dj}<;uws2v^>uu(N(-t5g1!_&#l=y1M* z#m3{+JGd3-s09zyti;Tj*CvD;n?XtW1+}cNI`fWw@dwutJO!N^Mj9AsI_snQi=FRc zzU_j{T^2ubR_W$Gs=d9YbjIb(=g+FRKUMMUX!~*?zi@-^<}N-$O*@Zw&PYDTAF|-d zh3;}f1^Csfbf4Q!L_V{nWiZm5tMzzZ_LlOnnc8I@36C$5PTuSC!y!|Ymu^mCjI}XV z?)DCh{BiE%G3vQuM0Cr&uQy~|{AMuALLW%C*P4G*Qz)wvO+4Pr>iYiD z#w23v`av@O84&ulLWg-NdG6CN9dUO=LHg0w4*P*R+zG_Cbd&67jGATEYt^*-Z&>l%a$`s)B$1l2MCjOcuG}}Q3nk#l=+wzQT6L(G`q(fUF z55LGNDNjGdJk-*tL6L6b>g;a;GG1@WdM=!JS1R?hKUgeTpUZ{y-Cx5tmI3%<0tqXu zAp#ytNg_(r;kT{76`K_KF`tM&>@xDXyl%D}Wt1^A?mlrgMVi^=#P5%Ey7Q|3g88H< zLTUplps6G5si$VaT+`Xv%~$7PchY@9E%*9R%`%zc<%A=2R!SiMXExE?;k-JV+6z)B z5>fdJF#IJjnf#JHU7b2KC5Z++HEgPReSiV%A=mty9BKm%$l7ISw&X6U2aZP#;qu%qUlZj+D=6Mlj~utfWr`B6m6Vtgck+4= zg!F{eWkbYIDZVB($E53;x5mJyh{4w}L79O=-n}j~1Ul<$L#_?Hrvo%g+5}{K(xTBGikEvz2zX}_}9Zb`J@JjQW3m-H{x01T=J&Ce_Ii+I1BMYl+W>7+acX>`4KM`1{EuM?Q&{=ljLk1f`Q|0_@Y~9sc zLdr;$?TPCMr+T`avr}4?o#-=j5aN3YsU4#eu>GCW4@X4hg-L25?|FPGjM+Ycaub2f z#`TsyWdZ@FiV{WE<^4H}Cf zGoOA-{7zKd9;$3Tc<4utG+EsX8qB7W(R^0yv{ z?U@cTo4&}Wpo`jRy;;55(~1Ze|Khy;EGL8M_OONTC0AB#&!Mg<`wQly?K{*Z&VJg$ zcC18G0KCqc=9pACe9k0UP;x4KRp2-)Y@%Lyh`+U)3C#HFxlq42q(z$5u zZdX3!HcP+SeA9ER`&{YlMSMc%9;ZnSCohC{ILqEo^V)o7vR zWV7MR`k`9!I|$lkyZx-#Ciyz!P<*x4sdOmTW5~6hlAMbm&A_D5I{JLAf99Svk=SAq zU&&ym;neI%+h8W^iC!Gv)+bX9Obj88OO9Ng^Yh{rgiM(At+BR)$8wT^tmkDNMMZNN zAYU;hT#S|_gtK~`;zFoOrENG7AsT}1@Q7FM$N4(Go~j*bZ{G z_VVVrrqY2~4T!s*;4u^wnmVDbqSXN>WaYo6cle^6$31!biX74L zH_pd=*$^4enat**2y75yW3&p+A2KImC@52N%#!rbFy<`sG|##JhHslxM8RjhKjp{@ zbVOqTUKHXlfZesfFuioUA!%GSGF|&z%lD<8tEv?$ ze0)L;#r8|-!^>4kHVJIEKd0pRd>ak(utL|xckCgKoO`{_<4uO0gQJ%H7q^{fK~Mbn z{P8D2nyNqxkqFrAYp>}#9r}lR<=4Ego83-uMZgG5d}7AG@dEbQ#AmO$L<^0Zi>1L# zE9yxw-44K$a|X&uUJG0 zd=k0F0@#F|1jZg6G}O*7?9ZmBd{s-;GO*XOAfz;Jq$R}#vf6P#qEo6+M0Qg5N?f&9 z2pyj{EnxLNs9RWIV_0o+*=g7&e&Egoz8eciBNGRIClbO9a-1{3J(Co9`H;i7_<$Ok{jV!C-(XhH;YW4QRXoA(K( zDoVFko}Djz3vofow)k={c7Dm->HCgw)dZcesOJ8))v}U7>*yKey0lE@t2GG!??f*PqB6e_RLFThw_<{<;$|b zTKWlS;qx~?1skDl9bU&RIVa!;&*Qv~FE=eb7?n6X(Usm!I*uV8C+1kHz`o`qZ}$$S zHF(KL^N;;nGi@JnJg{V@Prb4oZe=>KgR6M1cGItZ4AfB5;Rd5?0H6cf_0c^Rt8(A& z=V)tD`R>f?4qVxsZLN6ij?% z_UtbRr`#=GH-EU7%PXyLI5YRkqN=V#e3l5vmAN4<2*ngkoM42SRmH> zTzs7x&@xGfiQe5W|NTCQ{XK!^-d@7Fcy8zo4`%Xs<5$vQ4V~_G`L2<4b=+P*Ee2*TiO!`LOfDof8G>M;a2a6cLzVUkbU;gbkqH zTEP;%RB>huvcF$={i+P0`^d1|9BS&?9(^;kG1p|p1-qL);FR1GPmUK#d#k|T^{=Ef&fez9T|?wEe=GFDsm%K{C(1OlVn z4f}XZRPl0JnNsE_3`D4e+v4C%OYs5wH|AE0tHiB0H8pGLOJidy%*pRyU;Boz`eV z?H3rj=$hNP&PMQy}<`w z7&th@$+x~XYGmp!;#g$renTn;SN7{Aj&Fm?U>?g;T4C}01dxj5iScGg{2Au6QM%75 zaUv8-v){nKiV;?<8_H>It{%DYqA0N} zC(ZJ2#-17~JLZnlP}Ml!ts`5+&F1_{=chq~ppQyFpWB^;Q3pM5&Z}n7!j_rnwM1Sy zI1R{s6Vc7LBoltbbG--t_7$}El-uJ4*Vo<;ET7iel^JC%S=!mPn^@v}-TwybIX7S* zmPsn;z0&@+aUIJE)%@i+xkyXxftE^XHn=&$D>p27#i17GuR?7s&O$ut#uT1Ht5zV3 zqiOo)rZ5@W#WMkr3qBHgF{$w^PJN|(@36y$m!Yw<{RD_ZTG8Z9I(>P07=kh%=WdPj zh`J~FIv2^S`EK<(Nk_J1YqCJ&M#|>^nD4s`X$n4@oT@Y#t6_7_5WABYO>>}L;@fL< zS2Q8AqSr{)@b~0|KM5j&Dm!(uCOvhQRdF$`oRu%;knOJ8JZgjKMKg)K!hFifwK1Ij zvsWfbh*<}=EHC$Wp(rXwuv~#Ry0FK$I5%0(eKo9$RpA9|L2WIXzso%u>*=2IwXRE#zG6e`ja9)%rb~6J-m*1-AiRO~FapR3QPJ~Htf~)Q z^tv3*;wu9d$PZ!NJJBBQK?G_UFca`w$rK(^lTl3B5 zfM6<2B6cwoUn|=yG?Ae?OHK4Nz_s8WKzg)WGbB6BxD>gZpN%Tog~-%~Ymbo| zJA)wZ$4f76N3NNA!aW#tE+Z85;;ARvA5gllisCrV)4@ut=apH#OF_pQk?K24Z1}u< z4rb^4_fi*5O5Dsat3|d99%3PU6&;QCpQJor4Am_CenVe!$P)WK-+#esQHhW6TD_`< z)he|EF5+p+E?$t6Me6!)M~tF$QWtBVAp^WD+%$kbc?aBlL+_F0OBsLHOJ%+(ZY<6r zGT+^MZE4OMU90?x^2+C0ULE%dYWBQhWld@Zv?T1uT}p}VxSg@3H^bi}n{SL!cUx-6 z3nf{2?fvh2SmIaB0}US;)&BX@R%dI6&GVK1BrJs(i)S4A8V7vwX*?_zuDfQO2R zH;Dn8BO{`aPn(I_>R%^T?rsG+pcav$f~6~yvX^|K9$Tf9r$^1OD@9byNsi)C8UT5j ztsqD4XcnZ`wM~WA=|H_bj&zdy`B@V z)6@Vsp9U1BLBp>4#(oEc(eLpJfez0T_1JF-o?mj2qSM!-7mS6N3TzQyD25g(Pg%NpO1a=(`eq)hC&QIl}QnPZmvv zr$}f}RBlBdkGp&;z$u)k5_zoI+9{oDl4A52?00$E-2Xb_n2D!*e8soi*vtBpUE*62i27-xX1FrCOZF3A4plBD@jt%rwU2XDm|IvIB1P&7=W@WyO&k*M_xxo3$-YYFSN zr}YP{4B}m~If9?Nil{Wgn^vaMvw3m=TAue)6b2}Cdc+mhXmC>pAWAeFfUBcpKKs@# z7{@^`r5btHmE$o!uf*!+!qA<8!{UE&PyRY<3zSiVuAnT2Sc@vKCFs?~X2tx;o*8J{*?W8_`X3H`paYj0H~xf#VIR+nDW2H;u7rYtmhGszy8bkTCub zlNTp5GVZ)UGhj&n9H%cEp+$Q}P>ibjW^6xLXHu}iej8C7M^#7iCEPQSmEVvgFkSO> zHs^vzhJGq3)FoeJYGm%1USFI=FzVzRn2aA5K!-Wr1#)=y+AzX368B-2n(aW@p0AUp z5Q9Z}E#VZC$o+DI-d-dkQ+UY>FEiY=Qk}D>5fbt6_2*XoI~CoDbqPjF-}YIPi#KF!XA@zyha5?zQ0BQAM&ssI?y0Z8t~iP9^K~*?3dR(<)&1mJ zgsRK@lo?~k>-m`RdNIy2>gwd;`=YjX)~Mq-Afe8)vtEl!;%49>%F~wjK7=910ZB)u zYbO=yIg%7qMoAq-BxqC~c|AMugVnVtGVYsqLg#4&r$heUl-w<;^)wD=tJlZdv0m>Y z1ur6S98RKkk`=#I)}CFEAXwW{zSA3z0W>vf%6f3$86hQv3nwQf-Xw&WYL=kC^xC`} zhLHmX5BX$zSTrZBNBaG!{JM%iq@46ZM55ffBf5cm%KC)I1YZ+?u8MR9T8wq~ z-s+cVH~=9x*21%~fR-GN_7bC1H;v$85E~glIy6q$jo)Jdd#1ykk=3&F!FfLxBW?um z(kfu#B96!HZu#!=wKGlfX#0F7sX?sQ`4_H}RmSK`zdUAYc@?FcX^4Phva?&L;R{w9 zc3E9ju8@W7Q)I*}4#h|mxig8@k9O5v+J?H^f#zLFriKUtd_RqSeoz&AosLLp!snlA zxo?+MY3kD!HbicP92+{go3^u)ye9A_98=}PKQ1yu#5HP^M1-yrbxQghYOd_;U>r;t zAp`B5_D?Jjm!40q7_x>R|DBXJIAyC#^2UL{SU{H4Ud^kH`-xYQ#40;T)*P#%Cu#Q% zgNheU(v#!48C>z6RtLwg0U=gIIw~r1GCw_?d6kBSirT*n4gkBJs4~n>LQb;tM0>SH z{NRf%XXfLO*RfIHy69v;agnIPU;U_W`=JI-PKNt|_(SKbJ%Kk&DyhT7K>df4`+-6w z){c+|0^-o&VJ1CS#7EV>=av4;`D^PJQDaU%e?W}tN8^F76xDyg52Fu<4yV`CwO)Ka zCN;V_eG8EL=_s226D&8GKHLc1Ca8zMPy5oQ#JF*8-0`X>8q061no?AV$>`>5MWwd< zbq%tw-Hd-@qa`o(#^Qp=v4lpK`{w42nvPyhmU6ApMT2WJ{*U+`lZke+91)9oW0h8~WUl%MRf?0EW?}aHR5PDpJ>CcUhkM9It z@#XZcM_fOXu3Vhd$N!*!9&)YjXG=~mfj!hP%}(PdnUFhyD6s7>RFSf?n(TIaEp(Eo z12~$(O*? z6X>LAz1slBXb~d$ZdDgMZZ+is@Zj-ugIyxORF$LAX^Wxe(vVB;9my~eZ~L!lE|LP< zgDhFVCKpT^Gk#^P^Up)yMXx?L^x9{Cf2GJ1j?+sNKZEecJQ3x8I-DUf*8KgrWdn2H zcjl@4ZTdE2p{^7$Lh6Uu=-jC!DFFTGQ}l8mK_TzSndRo@3F8KS44 zv#cysW5-~_%mF7AA~6JUcZvu>nOsB(whDLeW*eDNJ_fVF+t@nYpLNJE39p3aHjZaJ z=65g#S?yK-8hZDg8r)yDKF#n@o=Jd60Z;o0m63`r6?yi5^&A8@;Am2)Nj{I`6wR~C zm^Z`bG96jLx=PGTAI?+gd+E@As`N7#7OeLwqS@tUX0Locy4)(|YsOhIJjV%?UAtk` zkQw?rWA$MnrE4^+ZmVY@Xct>VpLHZBuhjZ$AvKE0hC>PAJV{x+b$hM_OIs^OVIVM(F>WTv=Z+u zj`dFo^D5<6R*A9MtQ(x%-)}!wmPy5?B*};A#ENOknL*B8 zyGBHTG5OqljyE`U*Va`nZ`h9Sgv36F!1v0{M8<4C$cMrO$vD|=A4)37o`2pn8b@rZ z!0Ap7)iDTua6wwKWEnq4EkN9Ft?^8Is!#95BG$h-40aHXDCeWI@q9{9WX_c6XY~jy zPK(Ciy)8R$^%NYvUwBi|zRgI^;8wVIgdsYi9FChBD~45pFmd+?I${v_8-AHuufg#e z|M%WLPjD+7nQKu2DuLD|*=c+-$!z}oF&`hS$`efQ8u0YeqT=WxNw~h$$o!x!?c&Px zsW9&=(PDwLW_bAXjC{?^AfD06JAB%|>TV7h!Rpk>6M)=(;fZJaL|nD`DEKyv5ICQQ zqTA>!`uX)kLUIy#v&z5%wvX^u%qb%Fn&5(nX59JAwXi8c1Id>puw2{OYzI24NN|4U zo*O0YD^fjUnkXuTDcRz=Bi6D1KnA|Xtyf-%_NFg%u;bTx?<99yul`0gS%~+a5GvNqc zo^Um_y4Kdt@D71%TQLTcINYh()?h?<>mO>TM%i|foGde<6 zg|4${qHdL-Hwmr3%!NiXUql8(Ze9n|TaAl9s^H;4o)6XNu=P9vhY;6mu8pcg%wKuaS$iPpXORM1@cqSycH7$6+VA|`el9}zV&n&GG znrm%@s-bX}_uQ%~QC3IFHi2WM&kJ|>9p8(LFmC5(Gt9&BB2p1jCJHsXmh1KENR zCnV7!7@XqOo5G_+SIkr?ieQD2@3$gU#yG&rR}Y;pXb%pm=myi81%P2L&sfSSN^XUb zYsKF(EHP$~S8R;=``T-gfnCwD5+n2UZ&m!u`}A@UmJTwfb}zvD8Y{Ve)S50Xqtk{# zTt^X}OtySGIWC@mPj!?eXpFLsw{h&A<_6NHgw|#Ly{O(+_?0!I=A+IzGK+JZ@e5mo zyYH$zDJ@JSS;nyjk)sWC!g?eO&YdY1jH@5Xqju>-_>+tTG5~Hw~zn?ZUT~sq) z99f6gK^1Frhc_}y!TB?gF{{IjgWLIY2gOQc2e%uDGsELJqHN(z=a+2LjjC4we6MxB z?kvGfqED59Uw>=%dIJ|Pd%yxdCvODE9cE zGSWy$mKCCyx|;6oPF#{a=&pN{=14( zM@8UqfvH>3UfW?&7#pjOB`q1yl`s%}SaNyR6^G7$yng;hism`jQj0EGAO{-YejyjM z^CzGT{6U^EYEf6jWnm~hI)CnR1>;@b)Yl&E=>Y!_zIv0l2+z+Qc|g}2+8mBBeuymi zj|W&B8HK*ED(SR|{qE>R*XK!kyuwl~aEN#m&~Sy#<$&DqXaRgPJi@xgjXt`}K$$s-`ZVzAL)arZP-(Bg4eIA^Lz?-LR ziQ(d+lE1+Dw(D=3Zid4^m*Ef5HIoKQE%#V zq&qtIC1?TO^>QsB*r+b|zUqBp;0FGy$fFrsc)l`6_S40FkG~n^v~wc7MJN#nX^Z2X zg9sBtn)j7EATq?EfI*0vdfy^;(nwhS{P3_foE5{_LZG@_uo0ukPNY8dSXCh8N_hALXZyGM&nGxNnod@W z8dRi0)Tl(pyFo7<)F!y;$l#4lzHPGp_O)y=tJJwxmR<>j0V1tId3k(*>4^WLoUC~r z6=Hc;E6iwhq#dlnu}Kq~#gFb6OY!0xP7<@xH5*&OTa}(}xt?*bMVAsQRW&woXfx5? z&3wn6atUu#E!p@rrJoVQd!+OOB44WJ-s-sh9Gi5AWPB1aP|K#YYD$*3I^rPOC_SiqVT-4HbRH5=;=p!b=%90;CM!hJU0#OryIA0js62zroY48-)VG;KhZZbbn zcd;=1p4IU@00^B2az3pFZm_)l%fmi&b5sfU{H!U~!B5ZZD`dUEQTM%KxG_A~w#I!U zSCcuhO;~XPrl!`N%@Qwut-V@;x`7{VY4`yI8#|MUK0LF$P$;s=;^8NTtt5FEK{@-# zAyrf!H3UjY$MN4|j>p|M2ymPly8HCYp`S8&*raZZs97y0(fFsjBd!26KE`T z-d)ZVbWcPlTa%04PpWm83--N>tCToA9BvJ_0--=gO1$*hn^=bQ_}hoFsxy@g4G;4H z=LWe?v3S~T_uyUJGPC$_zl1%?Fr#%duy0{4zt4@g1KRqa%E|IezJ}zAmYGvQEX(^P z1#TmMEXWJt6z%-T_4Ze1{n;$2*A^hnhF}R?B|1wRjPJA+G*N)i;%7 zTBjrFIyHs%TpK^*Bh;AOC**z9QgSVuKm3(AUNhUi4*S3|ng z<;kvMMn`E+Ccr2Cpbb5CQ_##Z6CK;13y@XY+LQfWP+o|+OGo$GI@V5@F2iHo2nen@36iWB*o{{%;K;JQgx)q@qpk|x#`4_-wO5Ms(tN+ zpPkwZ#~Qy@%M)K7VQix#Xm_=(M3=80aS{{2f{r0SMm+ zhl+M$J#aQPUiXbT)anVN|3<=Tf4te&c-!bYR*Yp!&8OZAY6{ikk*ZCong*TGiKNU_ zR=ILXV}a-Jc|IB&>^Er{DydGWwtSyr1^gnwyV_N;PH>vdsNNwA}I;~1ne zwk%ZavNb6sbH$e-X>A9ykJos;SNN{dTlu)XNT0+MK3-w4GWR0CMFd4iRt5yBPv6w* zRnp^i_{Qi*3hTrjKBVaBk6EsDGdx3QTX;~Ls}R@JX+6^vhZoCm7xp}{bY8ZA`W&Rl z8mb`k2pO^-h@Sx4j`80G|1}Gdu03SH2TNY+YL?;FS#In)sloKOE*on=GKLJfaboH$mzAb}M5?utX_D zH$s#zc@4Rw_Kye%Pvq*+pSbW=3YV?pBC@43w%NvS zh^4<#vEMqpwr27$nfW6{g?5IW(qz2DgXmSX>ijm#f-eBfj~D zF20u!)JCVWp_kb6*Ae4jyL_C;Zo${uEi3uuk>L9@mp(NL25vu<;Zu>tVErMP8eYMZ>3IJX6Fc@G~SC+~wzb zMHPSl70@D(?ADgLIv@r(s7@!8W-Acm1Kv8yo4@Gk7mVyR$={AFklI9X~G%#HEnZ z>JG)qu_HMUrQxnrbsB9eK3zzisIolMam{QJ*!4;6a6*cc-q0=^1&AHoXuaNus=ws@ z?v*F7ZwzR=>2tWq<77?fXV*l859J4}6J1va^!VDVgTiW~o|Tr?t9UAEm#|MIzqtHz zCK=#+{*q$3`7NIwMxc@VF2v0?HA4O(zD-y@7^@Fsm^8Q6VPE? zty>&!N^l<)#gx+IUVh}FCuv*3c5@;7@N%ka8}fJr#QwmEJwL4GXmrH7c%UL-lO2eNp;Ic$cCS{RiUTj^`hgg;a`%K zf7jntsi}UgX39q#b)(q$N5SYHtv0>~4v386^dP2*9^xS(CjwtX&C`xqjGGm!YKFR$ zsq@Q5kvd0RENmI&kq@pJiaRcTxp7hH%ewKK$|5IoLkKU$PmWGG@wQC4K3Dl1T%drQ zi%{E+_O@Vev7sJzMr9740G;7t|Ip4au=@PA2G^VEW(&%mxvmka4s@nZhG0dKm{3 z9tpTr;)l>fFtPhK=dJ%qy92dPmvXH5kTF9Yyz;gl^sqHHz(|n>L>xRGW61v!e$jdm za30H}M<7L0P%x_ENY{NIt(i31rU-xf3(*U{?CC}lsHKE`yjXj!rD1wdHOg)4!V;zF z3YL(mE+Zi&P@^?Q>zwOJ)=GG@b+8sjW&i4F;Ul)oZ}F$b5tXGq^gwH6#tCjKal4bp z!Fuw;*Hxm+y?Z}DL870$44cCm7tdoy(GAopWX@FIuRDZ3dg$fxsRoiemWH2XA*N2l zD_7}Ga^E@vE>AE7p^2 z6F!X;vvXNYwcL#s;E*OYeFdaVOS&L&usS^bKBj~-a(~CjSo|2&Ch;5T|KC^ueag!y z=fiJeIO^FsMK4C^eaaqPYB}DeFErn&Pzf@YZ$Z?WHSNMz$J#N;Qs^g&VX`4B6Xe;7 z2uzC^ApC&g#P-kRWX5gZK5YO|zlkudMbtE+q$1>_raF(EcKx|N-K1ATV5P*-veIRV zz?0WU^eXdT(--SY(eARnJKk#qV*}K-*CziUmw9jsQq&LzLQf*!!;iF89G2G_3<>(7 zPPIq+Vf}P$CShNzhl9-z5gYFIK%cLZyi!zcv>!6SsO5V3`I!vM-QO9&`|r}Njz;a- zIFl1v>ITIC#fHbo6=_wsJk?9Qk$IEGh;o{S&=j@Y*BF;yRWN;=XLPSaYi;&-aYA~8 zMT6ruZFrQxsk)H;))=>T;UzxUsyP4^CmQ>kk$Xa-41JNNP#XVLjn-is&8C)&`{1eD zXN_Evwl-fZMx%x;nQe1m&qAbAdB!RIhE*(J)%`SFnbLjogXkZv#^~kjQt`_a%Y$`( zfB^QC2#{R&L)}%@_)J|fg>*YL-}Uzr>J!7q6#)CH!|i`Hb)Hd8bx*ig0Ra&ZL6EK@ z(wp=euz@0=AYB5e5D*a|AUz;ek)}v5QEAeNUz-nGt$ zoSZYWcapO*zjRcDdI-}hyx;6AiN>;>Ke;c^w5%e+iZ#W%rr*wVU0 z-f>`yrlida;;?5@%}$JC?M_VeV5pPGL^jG?K4o*oZP+3PmD?Dwy!bHWV8s!g&xX8n zua9QD4Wh2@4rWzunq3tyNfW4`S{`qKigHq^b1P*xg%_-~3J229%Y;$~TNf`xheO#4 z$C~3c%Tn=|f9d=hjPYU%MS6W$PrAjBLuBh*}0Q$VabQA4TuDfOcTftu$ml>qYWKr*6 z8CxujZ>5u~_m*6K<5W(@p#cxtcK0>I4Al*yu zRAR{M);X+1EyeNeC&jhpl1D(5REMf9P;kfVJ7yP$_DyCoe`b147F!?IzsH^;r6POp z^25b834~2{+7b3eFj#26<2viUFAO~CAjMDuKim`p?Ckcwdte6Z8CtV9Z)z@#L1mf#JMf~1t#2gJDp{nEk%OW9%3h|QKy=}lzMxE$f)>P4D z^-C|4olNniSHd$&!s$_Xppk(v?%b*`N}qq zw4()c*W)0yfi@dlERMZ`tu~sHtK%hX<)*J}qxWUbR}*vJCj2|knv@fOrMdo z{3+XJi4#(i2lMO+H~ST}5DLEX=|ruz{GdjD&dnt!@O6pfXODYI?uQzzrIg3qlDg^< zo|!ZCHO%+s+cqtYV6hbW=Psic01Qqf(U{$0)M=mg9<(`GsgJ){(=n<4*=syG`+~}5 za7s+i{IlmS42~oasAGUw1&LnEq{S;$Srv7=A0K%L>}r&7Dw~R}3HP7-R$sqDU%3M= zYPEj%%ah>Tt`0N6o1|RGsbK#$v#nB;l$j}Lz40iXCQ&S5n(Bx?==G`!X7gT0!jEQR z&$V&qyg~;K^23I3F3%+T2W)lG#H$d|OAYl)d*NCu_|sXx?)jw0L*@5w2D!v7=ct~1 zcIgY~mZn~Lb>|r&r9FpaMepT{YA>HZ!as_kAKqUKK5gzqiqrL- zS>n-J8g;#1^d;%=>ATtH2kqs^){b)IsmEJoPyNRj5#QVveHz-wPhaRO;1_+a_uy99 z6)1kyetnJUbRoB<9>w`2gl~pP_NS+2lA)aD*Rz19EFA#vD?^64o(g}Tu7$LY-`$Xk z4$`4bB)4HALGU6=e4+A-TN*{zih8-#%|vb_EhgAJ`UMzV*b}v&g zpMStN1Okb4`E=z?0&;^=jS&mDb9)B{w>^L8nQ1JOng`d4CQs0%*Q)$<`1$fZHo2u?D1!*Fr({n< zI2A+uIUk4~In(y@mDz+9WA!kSl1n3om7bRof4S+zVzd4Be7pVRSaqnLTm zTRL~^XdGhz-FW3mfz80X{sLLsMO5vhV9pgJO^~QYhc&I7`tit7g5#(kJTg$jSZeK+@7h4PuClwPuh=7^yZQN= zhmBsc{OjRa?X>F^&z~qprTxmATjDY_O3)1#X1|}OB)9YM*^8E^V&iYneYoY4;@Y4& zZP_2-Q^&hIJ*t5w9V8XXA-Ko4qDSFSmKGQ4JQVwEnLQ|~k+8#fk`H;2eN&@#tqdQ1 z;joP(m?+nTydF`#0r)T_@E{|`0^CzzET7J(rFt0NYducstW}9I& z+~LcIxTK=DA1+Yz*zUr8DZO~gu60{|9G}`xHa`P~+_-c%lCb9Nxpjt&R|reH$d}!7 zx;t-7!d|RS$&qjLuY^JPh%wdf!?@yrYDMnP=pQSC5+aM18&5(G&>YLb8_PP{#o0C9 zIYi-Q=!caLuJ=cb~$JGbcfyCDy5wQif*W~N-8X?xZBdhnX} ziFoufp285xhxN6P%r_!`PQ23;dKFcA!39w>n9~)Gf?mRH;&V z@K+wPYiQxEXtr+8Lft!Iscl+p)Zn{h6QW$OUXVa%xkkWE*}FU1>*7fY-_4y0Bh*Iu z<$1%UNmFwNE37hat^+R^UvI%@04sW^_l3X=4emSa@P zyfl5D{F|Th^pkTov}4C$%qY4sq43Yai30Nv{0F|%nXP`Wn~TC;A$1zEZ3mwp4af#5 zm6g1Nc|5L^Pv07RFq>L%V(=8)_)9X6K{B2}C(|JGs(7mwnemk2)pKclA#7zjM_7>V zpuOILbTd8^S>!UYUrZ8fggS0lLErPHDQrEnBJ2{}Q6FbeZCm}VVe{W--@u6Hodf20 zLpbqG)o^3Vi;KAJqdRdF6^AhR+J>M~MkE@&j?wcSL%w_M=Wxxa40$LdZXLK{tr9Eg z0OfhV5t`#cjX8Mx(yB9br&1Qr2qY)qv>peXstOOiyng%QJpRLL6`yNK1oc3g?t2TH zPbn!HDk&hBp*|_FwuTXJSF&9*S*);0P3|p#SG+e7es%-Xi2^1--#B1iPD{wAmf^Zp z`;k&MBAjk}AJoTGd`01Ng;qn&h5jpkZvy{zeD_R56BGB_11X4`p5xjU7a=b@tPiOZ z33b>b6hW-CiqE{Z1)EC;0xK%+AZHA*A_S!^>!_s);#dH&;}#cBC+T4-J@FA)K!m3aiD{{gVL8S_HGqtiByp$qOitS{o+}QEzm@ z{+eZ`P0vIdsUV#~sAt=5Yv0oCGw%-St`>cWWcXmX9C~unrDa!l9kapuo}qzu;B^VG zVZZ1Whds&|g`GA6sKm6s+RAF$R#xyr!D9CGI1wAK3fi8n+t*!C2yasU7HrXCDhUkl zv;0189u&`q$t32ow$F5qbbo$HJB|$yT}Iu`iQTDdoyp^K!pjuc&d&?=iWW*7^dsxa zH5Hs6k|(=L_ioPrK6(XhnyvJbOL3e@wKM|mbdMio??wP*?(2=qiIjY2ULKMqPhy1B zmY9YQV)}YyCB4Aet*RAgJ`XgjZ!#!}Amiwb; zb8&KSW4;!(9SrFPFEXc29<<(e5x1pKF9!%G`lfwYOo>A4Tf&F3)gxEK)mA6iI`&-O z%)U(`40MJqv6=F-dRdog-wcB3(?@XvdC z9L48GcnwQRh7CH$fAn|YSegI6_dC!Sfa6w(pFv6oK-!1aBmC{XVKIm!PO^k1YS2s#OG!*t5e>xG0 zbUtq^ThobuQTWjLhp8X0-}pmAUHLf#{RFCn{g$5?bZZ(@EkKvL=@vvAKkZoYbN}wK z5(eA+eQVxL90BNN)p)B#zgwiNvZq5j?LX(lmMCxjbowFY$PuRC{Le#--CfJbyI`(= zKD7R?kM)d>xPiv_ffn0q)8wg#{eNn2UH#X14tYNevQ@hK@>P9z8|!CP&Y|(ff}L&j6W! zGu+!mB+3j8u}*x#Ep*VW|hagc}_VfSc}EjF?j{NB8>^xT1`7Ckcq*+-jF| zlI;Eiu>%3x$-SWvn|Ve&jCC?%W!lkE8Ghzvvc#CA?6)(!eF9pEtP}Mhl zL7L3NU@7o<^aEIc$O=<0z^taK|3Dy-?`P|10@dvH?^*CWzLQ{~G?;Cv21+AgRJ@e5 z^+6g4wh0X?N?DT z&_wfPHgs=E@({@f0GU_4EUb2Q`Zq#kWK6l>`ko3Yt?H@{ir_Y!dVgWn zRu#m3RjTk(Zcf~>_l?rl7cetjK;8W?8N^fbRTlDU7uo6_Hn3SIX?L1|jlX&abah0R zAWy9Vp>K8gG6QQ^oXjNFgOg|X`^!v#fw;x-kD}N+h75&sn$d%)DP$2f zlF*V?=o1{-nrV>0H>H#&Wn4{r&bkvEp}E=Ts`;52%o_{wlmwpe{|rNWV()icd6)|$ z%u^E_C~+=`0$ib#+?I10~RK)rEgLS^vQItGI>0myvYsBNVbbY~&ZJrt_LT0TKptv4wu*Tk&Fd<4ogRI+ro`aYwCJ;WcEXT1;pQHM5 z;mfDvE`7TLg*IrO;Tl`NNlE|p02B474K8~x*(7i#i>a$|{QT8cy;~6%-$Y-!s}0fX zsf|e|UzJ8%cnd!~hMxd4N`ZedS+|o9rx8cY?={!kk(E^e!Ps{LeG|%uKjMQ~!erqR zN=#n0h0wyCI-&^ME{&VfyOmba*ta2cs&ena+B^m|L~#2N3Z8@f1}KE*O@Oct^dJ$K z3;yOoxC#t)iNK#)*B*;QslhG#Hc%Vc3m2oCRLu74q+d?4UckNHgkgl+k!x{Lv-LhU z)0t16)|rvwgRW(a?fa=!ezDphH|#YSIA%YwKLONt+MjcvStHZ#Yyn_&WPi1U0U+`#vIoa2 z4l5Qc)_5-o*~C;pvqq`>hF0!T#uo*cU>inrfJTw>sn@ImzuNI%M3J(rL+3|@s(P=U zA(<=oJbwyQNT>#R)^EeIf8 z^AT6AikzKIf?k;97NC4Br|YH`O-aE{vt#GfeMn!L5AVjI1i*D?Lr1bo=HiKC46^39)q0IGL(?Afj}Lx+&&3!&W}qRu%oKLn?T9ml8ES>Ma%@6Vwz3^n zP2YVIyQejHw^k34&g)mBzwo;n?y>A?%0q&AEQh!Z^YMl@Tx9av10N6U2iR1MLMvNM z5nmM8#|D`83mPTLdfl;t(3E5|;<=Ivp>}=vm58S6_m7dY{tbx|6OiZI)udB?KR#0f zCzI|v005F<-cGT|llMVvtD7gl4^BGQ%i-g?kYj(s;R%Kj$6xO&gysynd$;A`HF-j} z0=wd4)yY4?j&7C49z9%Wnv&>Jt!&O!XxRNqJ^z_Zs|~NC;Rv1x1Q1sdFR~3q<>ISxNXXM|37}YQu)G@b@;eN z{~x0?KMxbO%D?{pf9$gFaN~=^*T3e5-v*>WG-&O$|6TcF+L<|N7JKTSP5yXSOB6Bd ze@$W$Em30F@;mh2J@ofo(JIH9{cA|lPpPPxC%3D6uwK{Q^K+#p(|LV?EaEghKfJOi z(1S|aVMoj)z+UudtAwp4K!C;Ejebpbjyt|D*1G!=ojFqsu zZGprqo6j$#KnJEibuVMORUE}#kDJP?w?4%1jpp^p5sib3yyeA7HprE0Zw}My>l<{( zR*xHXl4h14iJcy=!49tQX29}q3lP={>l?Q+yIagQ0$D-d$ssXu{%c7VCPyX`2&oK< zEzCf(;(F2=JW6aaw6La1_5D*C1y2}-N4*IYY^tm-)yvyiL`j|EB6k9+FqQ{ zBx7t@shvTdrScFLBgN%ZPeivmQ8!iw-Bs3LBDZ|$xR`aU?ah%pu#J_mjJ~?V>V(vz zw(LvA0uH;Zqibd_3MVqJsox+!#`15MeJLsOoZ#=YNk2)4Ig!J-<&Hk#Ge;_DF6 zOMZwCZDvX?A};J+7J|ED;n`0u|0a+--g#}$x9e=-G)z95den*^cdX`~T#+X|IiZ%R zu;A**q)-fvIo|)svx&+#b> zRkRJC!W2N^(8;sExf4eXR+EsvSOw+*QP&PV+u2%OE`H?u*|H;7?hxSZl$d3~v)3~- zUPQ$Mejz6)oNE}l0MS*>-i;ijb%a=^sf41}s?DSMph{ohMu}Zp&ZI;pz9|_S#Fpl( zj(K8X&#t`=mZjluJao2%=L&V54-3}5jX3o|5zZEdt}AA}D&u37)S3M$g74~us~dn; zmjN&&NWWIR%G7OBVcnerM)a^J4jvVr{lvF__zuGRUU&2eWy|=7M6qspSHI|@OaH<7 zc!R$w#q#PZ(+Jn*ksr@ojd)@Qy_*Zp)!QUI7voTNpPcdC4|@y~d_%*(MoKp0({SoM zRmVC5J!XqrZ*b1@Lx&=3W^^_Xn=f1DrV0HN?E;mCPiTHIb)*Cm(VB4+n&HFn+zETa zy$3*dJ;)g^=Q|F!1LDdbv5);01E|1h2&^TCZJm+8<^EV9mhONeX{99%3P z`p6$?6pF7LZ6-81!HKrVteR)|Emx#0D{;NU$1!NWRmz{nRpVPe&PfhbNEXM0R{M-eIuYde;-jX5OmB~alUH=tua=eF} z7Ue*uHZ=}cHa#x{0WpvL247Ou5@1b=27xDMC&s8>Kt=xZ;sE6Rl^gxS)DZUw$Z@|0 z{qc-*bcmrHX<~-E{9pNTl$9=$M^Z!ySQ^_9{?Jb*r49Wn@b)E|p4W@gKq~^ugO<=# zMB6?^XO5#Bxh}7ifDPMSwg*nYKE_aY7@A2c!}HvH_L>@iNegRt~;{3Sc{rix7r~PVjp%V?4Y4+duj&AN~ zG5_JTox9@DF*9D`T>wBw?nL1J^lphRqzZiAF555gTTyZ}%WRW+eU9^*yI~k!i)NhI zs&bp-vXGZWHsfmcyEaJz7m#uEIQ_EqYenGBE@Hm8%k890K;a~|T`_ZQ$DT$V-j%0) zXoFU3Sb7BJGW>|%D?S_Q$-JhjpgI*8N1dlW)-iCj8Si28Q7VV7p_6crZ}U2p6zYnk z=@M3yEgipgqBgE9gAD^mqFV4P_}}_D8;3(BqEL75QhPY!&0r{-xg%*esr%1o8k-4g>x?j3M zn&#f(+N^2$IZhw=I~A$!kq2OG(?nV7`=#cH(zO9iiw5T^&tS?Eo1<8&Ga}_q4j-o; zJrSU1@9PDHWm)Bp4%Qh7$}NZTcLR&Q`k_k?IDf&|+m77qK)-7UX&|yR!X2dDL8!tT zY9q>M%Gc)CF_8M%si^QEe8`MUhdb@?8+tep+VX>7_ru<$D77Sgcx z<#Im`;`yhWT-&PhHQYH4k^7%qb7u=|qw$|%E8VblT-EMBa2jdHhhej1|7j^9TIlM( zbkj1T6no*XEE8P!iE=DZ^^dp?ZlW}0v;LEL4(a`Z`B&O8v|jlSfUFu_P0;^IJc+3P zM`M3cP4NF$ap>@LaHmJeir#*U`ZTOME|4Wh?s@$e6%G{*)$@5K_0j=EqX4xL^Bvw4 zSq?`8w>|``fx6_>e8*C34eQ?%zhWF^~Pz;uVd-rHW9B zeubdBB2BLheVdy*ukJ@ZR6K+51k%P#52k%#vJXUgBMEUvUCkOOPJG;3DElEuTcZlh z5e)1k4dHd-OmsV5AxYGbqP}CzIRugcALm=u;d5K`JIE8ELZZ~y z6owpDtAXM+q2@;I1g)PrlC@rfqT6Ed#wYEg>VGc5Pi#kbz~#9mlL!$VIqtQ%YaKgH z-Mi|mMDNLG3x$z8?P(BsD@%w-IO-(&(T5f@QH-Y>xM-2)s{v^9fn1p=y~b9r;-w;V z!gRQDjHhtyQ@52}?(@x>3i9~nA6tB^LND8ACJE|kd*=BHFLpFAj;6`~NhMuYQm1c)yN~+w2t$vj>K6 zW~<10ja*Ry(N!;n(3UR`g`&42r%|OfMWo)ci6)>2((i)=O9xv$`7nD|ZJ$>WBLJ?9 z+$R~M9Rds#LC%3ES06letS?btV^Wk5_!tgv3ZFzc~_%VOz%Z}8aU-+>!B}p=u+3u*)?PZ_jFJ{b=3{J zz`gl1dl_sbWklD?^v@e;3ml7L6mC2|X8i*a@4bafhjLS^bAz#%YE9Vl8kA1zfVQg=( z@Vh%}k~L;K6PyYRyo0s%XL-Fy97qm5+FuY35VVgZtxU^4^y*;y>8TI2(4Efk=YhNw z0ZYs;QOh$>M9^Ekl2$2sg2MCd=YrYp#F0k;hFI*)4Ng}OP-s2rG;M@&#V?#({W}3B ziM=AL5Z-+Ko0XVTv(V&Tw}D79O2aqMW0;%DHH#EIKp|VBFH|e^d);PwFg|X@AGVcr z{bG%AD0+Lp&r>9gZ&*~0oIm$2ohJ-wuMgxh2&;Zo$ACNa>?B#;zTr}YH&SjtvP+tZFao;yo0ULzk68 zot=2|P@1vi2e7mBXW)V8Su6~}uQKd|fBIcjfrFmP3lN$C6=s^868k*rx@4>#H`MH1|V3|*#y;tc{$?xl*; zl0syFoq|tUfHe`VitB7MOIvtYATe5;rpJl2CzINfA#J%(qTvQj=LD`2GO4Uw4h6l= z2xb+&{qyM4hxv3tnkeeiTIPDbGr0|8K(cZtW;>HRghDB=I6lkL!!r#4djeLYd`dK>N)9PzL#&;n4r0213(UY{Yt z7il?1$?W|bIf4N?vL}PfZG~hu-M!5$0y#?#FPXgbX*^Z?OENu9)BN{JRM&?a9N*~3 z@`KUuX$CcmGhs?zK{cm%wWKTOTNX^QthJ^9kR;iAA~qRnrr5(i-!2;D`miqgcp^~t zX(H=yv7u!n=Oi|OI<+PVqSwgy@@#x3GBPoC!XfJ^Q>ky!XZda=#7I-2uCX-m)%ui1 zx2ByShTC$t_S`tJab;7cyqzx^u(#H@@*;^_)UgEsQ8O&<(!K+rj0p5k8^`pWQ~}HD z3s=7hR2vC$6%bX7B)s@a9IOure0Kb$p>=bIcUOdMLAs(U``sapUX0rgIqs+3X_vS@ z9Pqai;y2#B{xJTJ$lr2G?Opy=t#W!ngMK)qMJD|Dr*yPbBx;ubEvb0z9W(oBnTv22 Ux!TjW&N^}Tw&|_P8%`1b2M}vu@&Et; diff --git a/titles/chuni/img/jacket/unknown.webp b/titles/chuni/img/jacket/unknown.webp new file mode 100644 index 0000000000000000000000000000000000000000..ebfaba4245ca6ec1cf2a23edff3d3bbe694a8265 GIT binary patch literal 6558 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%0AkF3g0HM?Uo#&|3HtdApZ=W2{OlRo*Q$I~ZIKMH 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

}D)u&sKM~iX>!@B1=(;vPDsL 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 eg`Pl$dnjl9oCzlbMFb)er&gisR1pWm&Xb$KA literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank1.png b/titles/chuni/img/rank/rank1.png deleted file mode 100644 index 552b12cc51d73c7ffa1f69c275efbe047f23aff0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33652 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 Sqd)0UoduTeDA2$3HOlBVg1##UX)yUx*o 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 W=Ak@QZ(;SqnBnc}b_Uk`0001(hll3? literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank10.png b/titles/chuni/img/rank/rank10.png deleted file mode 100644 index 14d976cbfe0bdb9dbbdf0a623b3f6cfa8b964ac4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30558 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 literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank2.png b/titles/chuni/img/rank/rank2.png deleted file mode 100644 index 0c789389affe6ad75c78531abc9160b29f2fb705..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26546 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

eq5<(^xvcf_jBO$F5`5hK4T8g| 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 UP>sI5_}AxhQc8DAB#eXq7ZVVjO8@`> diff --git a/titles/chuni/img/rank/rank2.webp b/titles/chuni/img/rank/rank2.webp new file mode 100644 index 0000000000000000000000000000000000000000..df6dcca53cf441f90d56799b37398869a63279d9 GIT binary patch literal 2444 zcmV;733K*RNk&G52><|BMM6+kP&il$0000G0000`0suV#06|PpNZSJd009{?X&c(N z{xxqBBKki8?f6JT3EKWck~C}Ewr$(C_h=n?(`jQ*wyl$G+qP{RSy?@-n)t8YjErXV zFEU4sfBe=Jx&N5zb!K$Vq9bAgCZJmi4+XDaVt#^xO7Q|RG#G~ZSYzU23@aO6?3@LW zVg$sBbvO$MpvHtSvbs2qV>cWNB0hpxvCiwUn3Z*PBgyl4M4_`zkOBhEQe{-Y4`rM5 zaTE|hOFm_E|K|A*V`!8(cNWA0nWb8VF_lZ|qczg*roDu8H>r`olyEIx7z;S(@Cw#u zSv9Y^IZdfjq1=stj{pJiAs_%0P$2S&00k7#q9PydF5)AA_y{0?00IbL0RhBkI6wwC zz?NjNxu&M3brlW2@%4K0E&tSqOpyqBE>lk-fs zTg~L^>gu`~3Lu1G*s`4b6cp_29LFe(iVqxXEgr7$n8nlqRzNbcaB6>^4DX_L)*sVy zcDuU3d;mJ>KeD>Ip;?e&I2pmr-t;0AHnxKd19NF|dY-f%+ul<8iI6HTlN>UxlpZ ziJ2M|@w(9&9KRJMfO_;g-x?fuP(Y)|Q*1K^=jrIahZr~wW8)bItHJwdFThStV{B{z zn268E^dm$2K=Puo)FNDD8wW5nc65cIz!GZT)&8M0`UFankk6|b;6&@%(;x$5;C~ka z7?e|3KLGC=IG;)_z=h_~wxtP;!l+1ZbBv?m9m0chh$2vuBpJw%W)Jl17z!q(;9D8Y zKx&CV^dr0{dEKQ}v#_fHf{7`>fVP(7y&-XleoRr)tc&}dq2Y4pz}%ePW@yQicO*%q zs03-6zGNI7KQ3>3`E}}zjIO38s^`k{BuUZ$Z;%GVsdjZ7=Z~K@HM){50u%^<0Ehtf zQ9uFpQ9ykZP#^6sQj`GVBY*(nBY*(nlK~cRfB;g~H1lo8t;qTF11Soq2ub2F5dS>JmF-bZ`Vp1dm!lt>gnqo6Cx-c${VEhdJvlcZ^yCK@ST6^(3~W^XYz5=b;n z(MVAbDjJY>xwnNaW`-n?lA@p#R)j!OX%+$0nign5P>NMV1HfWrDqf{909H^qAc6$| z0FW*KodGIQ0zLpfX(*9Jq$Hu4c>lae286Po+wO>Y?*d+_{@3$J8_`zJG$S$iOpUO! zpG?I@IxfW+0`47@MDzcP8$?OttVK=a&pV-~4dOA(-lHEuI)H(V+wKYVY z`vavg&}qQwUoTOGs66X-F2FUxKQ&O31pkOD0DSic_YXv$f=KyUBn+cTla3LTmq#J5 zvsbuPqHnQ13|>$G-vmK9Ovu3$`_KPCyHr4F@2HSDnhO^H;G+ zFt({|dD=srqgNz0?dV)9L@Z|Cz)df;0u@^G`zBPQwK-b48!#sGrr?P<0nN?9rY|>K zcGtVomModLE#aTw#*zCjsU3FvhTZc>&8!O7NnbjUHDyzGp}4qtU>tsTWlZ8EcJjd6 zgaTKAzbwOq^BI7vw7L=r3mJd6u^05-AU|*rTK}rrng9xeelRJe-{u2~Ep93LBPQ@q z5~Iu{JELLOHA^Xd5uy0>L%wq2`aOsi5Fu5sEiu0g!MG(dJIjpT>r_2bi&Nhe2&EY} zU@eJQgD;ZWl@_sEIB4-wjG8qoV4JG>q}@!|(2RDi+Z4P2t%N8;@;qm$qNH zIaQCKwdIrc<~If#QLN`_+~M|Cbcb)i!xsHU9Vv^0XGXnCw|fc!c1 z)V~WRh4>&W}y>8P=ZmLQY!%BE2Ax_<%QRKVmq_J|Hsj2~sE6Klog zUWao3{fV|u{)nZd1tJM~%MD_n9eR8pAgr4T=SHIuIS~!!x49Rw2B+@*3#dfwvYR2H zS_yDAFf-vPi(;%75J^}9_71pFv!^Cj5hm^GWgr3ji@cMj8C)>lV#x^h+qm>L!O2Y!N7+v5nN#mOCKuCj!po{bwlByEs&?i1C%mh9z?ZKSPFxbgOXDlp5edgRp=w;kPaFPX3w#D9d%W*sA$ KnT&i;0000NIB#tL literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank3.png b/titles/chuni/img/rank/rank3.png deleted file mode 100644 index 5d55b3d4a56933c0fe8a9706773f873e07a0b91c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30641 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 Op&+X&Qz-Q)=zjsTDjfp= diff --git a/titles/chuni/img/rank/rank5.webp b/titles/chuni/img/rank/rank5.webp new file mode 100644 index 0000000000000000000000000000000000000000..8a04d1c7c5bb7f9f90472823bc71082317025ed6 GIT binary patch literal 2542 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 Op&+X&Qz-Q)=zjsTDjfp= diff --git a/titles/chuni/img/rank/rank6.webp b/titles/chuni/img/rank/rank6.webp new file mode 100644 index 0000000000000000000000000000000000000000..8a04d1c7c5bb7f9f90472823bc71082317025ed6 GIT binary patch literal 2542 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 diff --git a/titles/chuni/img/rank/rank7.webp b/titles/chuni/img/rank/rank7.webp new file mode 100644 index 0000000000000000000000000000000000000000..723a10d5c50ee40ad6958f9f805b8ba8281ea935 GIT binary patch literal 5100 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 O2z98v!SwGi0002ANy}FN literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank8.png b/titles/chuni/img/rank/rank8.png deleted file mode 100644 index db7ae79a364d633404303a69262a1cce6fc82c6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42548 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 diff --git a/titles/chuni/img/rank/rank8.webp b/titles/chuni/img/rank/rank8.webp new file mode 100644 index 0000000000000000000000000000000000000000..723a10d5c50ee40ad6958f9f805b8ba8281ea935 GIT binary patch literal 5100 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 O2z98v!SwGi0002ANy}FN literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rank9.png b/titles/chuni/img/rank/rank9.png deleted file mode 100644 index 145096ded5dda6cdfae60f60b00ed186b6c3e3c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21428 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 KYM%1htN#lf2TADw diff --git a/titles/chuni/img/rank/rank9.webp b/titles/chuni/img/rank/rank9.webp new file mode 100644 index 0000000000000000000000000000000000000000..35ee9867f21b8b5b277652bab6ba2e36ce74a143 GIT binary patch literal 2376 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 literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/rating0.png b/titles/chuni/img/rank/rating0.png deleted file mode 100644 index 66813b8f2e4c3e4e0406381c5a3a7384fba3cf21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20220 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 IaZrE&0Q*A_g8%>k literal 0 HcmV?d00001 diff --git a/titles/chuni/img/rank/team3.png b/titles/chuni/img/rank/team3.png deleted file mode 100644 index 9ac472405022c70c37b02e1dff4a29c496d0ef7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45380 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�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 diff --git a/titles/chuni/img/rank/team3.webp b/titles/chuni/img/rank/team3.webp new file mode 100644 index 0000000000000000000000000000000000000000..4521bbb3d72fc6caa940e30fbc7140c8c9b721f4 GIT binary patch literal 4170 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 UKpp8qmiy|z{#qXT000000M&{C(EtDd literal 0 HcmV?d00001 diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 47c1939..0b96b7b 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -1,9 +1,11 @@ +from logging import Logger from typing import Optional -from os import walk, path +from os import walk, path, remove import xml.etree.ElementTree as ET from read import BaseReader from PIL import Image import configparser +import glob from core.config import CoreConfig from titles.chuni.database import ChuniData @@ -43,6 +45,9 @@ class ChuniReader(BaseReader): if self.version >= ChuniConstants.VER_CHUNITHM_NEW: we_diff = "5" + # Convert any old assets created with a previous version of the importer + ChuniReader.ConvertOldAssets(self.logger) + # character images could be stored anywhere across all the data dirs. Map them first self.logger.info(f"Mapping DDS image files...") dds_images = dict() @@ -533,17 +538,40 @@ class ChuniReader(BaseReader): self.logger.warning(f"Failed to unlock challenge {id}") def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None: - # Convert the image to png so we can easily display it in the frontend + # Convert the image to webp so we can easily display it in the frontend file_src = path.join(src_dir, filename) (basename, ext) = path.splitext(filename) - file_dst = path.join(dst_dir, basename) + ".png" + file_dst = path.join(dst_dir, basename) + ".webp" if path.exists(file_src) and not path.exists(file_dst): try: im = Image.open(file_src) im.save(file_dst) except Exception: - self.logger.warning(f"Failed to convert {filename} to png") + self.logger.warning(f"Failed to convert {filename} to webp") + + def ConvertOldAssets(logger: Logger): + """ + Converts any previously-imported png files to webp. + In the initial version of the userbox/avatar frontend support, png images were used, scraped via read.py. + The amount of data pushed once a lot of stuff was unlocked was noticeable so the frontend now uses webp format + for these assets. If any png files are present, convert them to webp now. + """ + # Find all pngs under the /img directory + png_files = glob.glob(f'titles/chuni/img/**/*.png', recursive=True) + if len(png_files) > 0: + logger.info(f'Found {len(png_files)} old assets. Converting to webp... (may take a few minutes)') + for img_png in png_files: + img_webp = path.splitext(img_png)[0] + '.webp' + try: + # convert to webp + im = Image.open(img_png) + im.save(img_webp) + # delete the original file + remove(img_png) + except Exception as e: + logger.warning(f'Failed to convert {img_png} to webp') + logger.info(f'Conversion complete') def map_dds_images(self, image_dict: dict, dds_dir: str) -> None: for root, dirs, files in walk(dds_dir): diff --git a/titles/chuni/templates/chuni_avatar.jinja b/titles/chuni/templates/chuni_avatar.jinja index 30fecf1..4fa4a45 100644 --- a/titles/chuni/templates/chuni_avatar.jinja +++ b/titles/chuni/templates/chuni_avatar.jinja @@ -14,13 +14,13 @@ - diff --git a/titles/chuni/templates/chuni_userbox.jinja b/titles/chuni/templates/chuni_userbox.jinja index 3bca2fd..192d96e 100644 --- a/titles/chuni/templates/chuni_userbox.jinja +++ b/titles/chuni/templates/chuni_userbox.jinja @@ -18,7 +18,7 @@ - +
{{team_name}}
@@ -26,7 +26,7 @@
- +
Lv. {{ profile.level }}   {{ profile.userName }} @@ -37,7 +37,7 @@
- + @@ -50,7 +50,7 @@ {% endfor %} - + {% if cur_version >= 17 %} - + {% endif %} + {% endif %} + {% if cur_version >= 18 %} + + + + + {% endif %}
AVATAR
- + - + @@ -36,7 +36,7 @@
Front:
Back:
+
    
Trophy Sub 1:
Character:
@@ -180,10 +181,10 @@ function changeItem(type, id, name, img) { function getRankImage(selected_rank) { for (const x of Array(12).keys()) { if (selected_rank.classList.contains("trophy-rank" + x.toString())) { - return "rank" + x.toString() + ".png"; + return "rank" + x.toString() + ".webp"; } } - return "rank0.png"; // shouldnt ever happen + return "rank0.webp"; // shouldnt ever happen } function changeTrophy() { From 5ba0c8b04c8f7dff997dbe9149c02776189622be Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 24 Nov 2025 09:31:41 -0500 Subject: [PATCH 56/63] properly encode allnet response for some older titles --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 011cc74..3cd7b6d 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -359,7 +359,7 @@ class AllnetServlet: iv = bytes([random.randint(2, 255) for _ in range(16)]) return PlainTextResponse(content=self.enc_lite(litekey, iv, resp_str)) - return PlainTextResponse(resp_str) + return PlainTextResponse(resp_str.encode(req.encode)) async def handle_dlorder(self, request: Request): request_ip = Utils.get_ip_addr(request) From 29a52d2712ca08440457b04eec1cd90e4c63c08f Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 31 Dec 2025 14:37:46 +0000 Subject: [PATCH 57/63] chuni: fix map area/unlock challenge conditions (#237) - Document all map area/unlock challenge condition IDs - Add conditions for missing secret maps in LUMINOUS PLUS/VERSE Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/237 Reviewed-by: Dniel97 Co-authored-by: beerpsi Co-committed-by: beerpsi --- titles/chuni/const.py | 124 +++++++++++++++++++-- titles/chuni/luminous.py | 207 ++++++++++++++--------------------- titles/chuni/luminousplus.py | 154 +++++++++++++++++++++++++- titles/chuni/verse.py | 43 +++++++- 4 files changed, 392 insertions(+), 136 deletions(-) diff --git a/titles/chuni/const.py b/titles/chuni/const.py index f5642e3..6d8c167 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -112,30 +112,138 @@ 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. + - "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. + """ - For the RANK_*/ALL_JUSTICE conditions, the conditionId is songId * 100 + difficultyId. - 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 + """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 MapAreaConditionLogicalOperator(Enum): diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py index 9af38cd..48f31e7 100644 --- a/titles/chuni/luminous.py +++ b/titles/chuni/luminous.py @@ -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 " 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} ] ) diff --git a/titles/chuni/luminousplus.py b/titles/chuni/luminousplus.py index d33833f..045e4e5 100644 --- a/titles/chuni/luminousplus.py +++ b/titles/chuni/luminousplus.py @@ -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): diff --git a/titles/chuni/verse.py b/titles/chuni/verse.py index 3c933c3..55a4058 100644 --- a/titles/chuni/verse.py +++ b/titles/chuni/verse.py @@ -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 From 2cbf34dc287c66846c6cfac7363d34bbaa550eb2 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Thu, 1 Jan 2026 21:35:23 +0000 Subject: [PATCH 58/63] CHUNITHM X-VERSE support (#238) Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/238 Co-authored-by: beerpsi Co-committed-by: beerpsi --- .../versions/8b57e9646449_linked_verse.py | 82 +++++ docs/game_specific_info.md | 1 + example_config/chuni.yaml | 9 +- readme.md | 1 + titles/chuni/base.py | 6 +- titles/chuni/const.py | 44 ++- titles/chuni/index.py | 13 +- titles/chuni/new.py | 2 + titles/chuni/read.py | 33 ++ titles/chuni/schema/item.py | 51 ++- titles/chuni/schema/profile.py | 3 + titles/chuni/schema/static.py | 75 +++++ titles/chuni/xverse.py | 317 ++++++++++++++++++ 13 files changed, 628 insertions(+), 9 deletions(-) create mode 100644 core/data/alembic/versions/8b57e9646449_linked_verse.py create mode 100644 titles/chuni/xverse.py diff --git a/core/data/alembic/versions/8b57e9646449_linked_verse.py b/core/data/alembic/versions/8b57e9646449_linked_verse.py new file mode 100644 index 0000000..3f218ab --- /dev/null +++ b/core/data/alembic/versions/8b57e9646449_linked_verse.py @@ -0,0 +1,82 @@ +"""Linked VERSE + +Revision ID: 8b57e9646449 +Revises: bdf710616ba4 +Create Date: 2025-12-12 16:09:07.530809 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8b57e9646449" +down_revision = "bdf710616ba4" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "chuni_static_linked_verse", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False), + sa.Column("linkedVerseId", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=True), + sa.Column("isEnabled", sa.Boolean(), server_default="1", nullable=False), + sa.Column( + "startDate", sa.TIMESTAMP(), server_default=sa.text("now()"), nullable=True + ), + sa.Column("courseId1", sa.Integer(), nullable=True), + sa.Column("courseId2", sa.Integer(), nullable=True), + sa.Column("courseId3", sa.Integer(), nullable=True), + sa.Column("courseId4", sa.Integer(), nullable=True), + sa.Column("courseId5", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "version", "linkedVerseId", name="chuni_static_linked_verse_pk" + ), + mysql_charset="utf8mb4", + ) + op.create_table( + "chuni_item_linked_verse", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user", sa.Integer(), nullable=False), + sa.Column("linkedVerseId", sa.Integer(), nullable=False), + sa.Column("progress", sa.String(length=255), nullable=True), + sa.Column("statusOpen", sa.Integer(), nullable=True), + sa.Column("statusUnlock", sa.Integer(), nullable=True), + sa.Column("isFirstClear", sa.Integer(), nullable=True), + sa.Column("numClear", sa.Integer(), nullable=True), + sa.Column("clearCourseId", sa.Integer(), nullable=True), + sa.Column("clearCourseLevel", sa.Integer(), nullable=True), + sa.Column("clearScore", sa.Integer(), nullable=True), + sa.Column("clearDate", sa.String(length=25), nullable=True), + sa.Column("clearUserId1", sa.Integer(), nullable=True), + sa.Column("clearUserId2", sa.Integer(), nullable=True), + sa.Column("clearUserId3", sa.Integer(), nullable=True), + sa.Column("clearUserName0", sa.String(length=20), nullable=True), + sa.Column("clearUserName1", sa.String(length=20), nullable=True), + sa.Column("clearUserName2", sa.String(length=20), nullable=True), + sa.Column("clearUserName3", sa.String(length=20), nullable=True), + sa.ForeignKeyConstraint( + ["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user", "linkedVerseId", name="chuni_item_linked_verse_uk"), + mysql_charset="utf8mb4", + ) + op.add_column( + "chuni_profile_data", + sa.Column("stageId", sa.Integer(), nullable=False, server_default="99999"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("chuni_profile_data", "stageId") + op.drop_table("chuni_item_linked_verse") + op.drop_table("chuni_static_linked_verse") + # ### end Alembic commands ### diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 423e36d..9682bb0 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -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 diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index f9c2f20..088f23f 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -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,12 @@ 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 - + version: 11: rom: 2.00.00 @@ -47,6 +47,9 @@ version: 17: rom: 2.30.00 data: 2.30.00 + 18: + rom: 2.40.00 + data: 2.40.00 crypto: encrypted_only: False diff --git a/readme.md b/readme.md index 055bd81..ab405ef 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d28a799..56f442a 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -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"} diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 6d8c167..a5dd0a1 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -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 @@ -246,6 +250,44 @@ class MapAreaConditionType(IntEnum): """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 OR = 2 diff --git a/titles/chuni/index.py b/titles/chuni/index.py index ce4e02e..ce331f1 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -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,7 @@ class ChuniServlet(BaseServlet): f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8, ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56, ChuniConstants.VER_CHUNITHM_VERSE: 42, + ChuniConstants.VER_CHUNITHM_X_VERSE: 14, } for version, keys in self.game_cfg.crypto.keys.items(): @@ -280,8 +283,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 +304,10 @@ 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: + internal_ver = ChuniConstants.VER_CHUNITHM_VERSE elif game_code == "SDHJ": # Chn if version < 110: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 45e525d..76221cf 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -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 diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 0b96b7b..b880094 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -68,6 +68,7 @@ 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") 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 +537,38 @@ 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}") 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 diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 138ef3b..d48f55b 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -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) diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 45afbf6..da29da2 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -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", ) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0e0f2ca..d85d3f4 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -310,6 +310,27 @@ unlock_challenge = Table( ) +linked_verse: Table = Table( + "chuni_static_linked_verse", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("linkedVerseId", Integer, nullable=False), + Column("name", String(255)), + Column("isEnabled", Boolean, server_default="1", nullable=False), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("courseId1", Integer), + Column("courseId2", Integer), + Column("courseId3", Integer), + Column("courseId4", Integer), + Column("courseId5", Integer), + UniqueConstraint( + "version", "linkedVerseId", name="chuni_static_linked_verse_pk" + ), + mysql_charset="utf8mb4", +) + + class ChuniStaticData(BaseData): async def put_login_bonus( self, @@ -1232,3 +1253,57 @@ 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() diff --git a/titles/chuni/xverse.py b/titles/chuni/xverse.py new file mode 100644 index 0000000..56f51d1 --- /dev/null +++ b/titles/chuni/xverse.py @@ -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, + } From 8408d30dc5511abc1b6b8dcf515bd81793570b44 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 1 Jan 2026 20:40:27 +0100 Subject: [PATCH 59/63] chuni: add stage import and frontend config --- ...rse.py => 8b57e9646449_chunithm_xverse.py} | 24 ++++- example_config/chuni.yaml | 1 + titles/chuni/const.py | 2 + titles/chuni/frontend.py | 47 ++++++++- titles/chuni/img/stage/.gitignore | 4 + titles/chuni/read.py | 28 ++++++ titles/chuni/schema/profile.py | 11 +++ titles/chuni/schema/static.py | 96 ++++++++++++++++--- titles/chuni/templates/chuni_index.jinja | 25 +++++ 9 files changed, 219 insertions(+), 19 deletions(-) rename core/data/alembic/versions/{8b57e9646449_linked_verse.py => 8b57e9646449_chunithm_xverse.py} (80%) create mode 100644 titles/chuni/img/stage/.gitignore diff --git a/core/data/alembic/versions/8b57e9646449_linked_verse.py b/core/data/alembic/versions/8b57e9646449_chunithm_xverse.py similarity index 80% rename from core/data/alembic/versions/8b57e9646449_linked_verse.py rename to core/data/alembic/versions/8b57e9646449_chunithm_xverse.py index 3f218ab..9ff77da 100644 --- a/core/data/alembic/versions/8b57e9646449_linked_verse.py +++ b/core/data/alembic/versions/8b57e9646449_chunithm_xverse.py @@ -1,4 +1,4 @@ -"""Linked VERSE +"""CHUNITHM X-VERSE Revision ID: 8b57e9646449 Revises: bdf710616ba4 @@ -18,6 +18,10 @@ 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), @@ -67,9 +71,20 @@ def upgrade(): sa.UniqueConstraint("user", "linkedVerseId", name="chuni_item_linked_verse_uk"), mysql_charset="utf8mb4", ) - op.add_column( - "chuni_profile_data", - sa.Column("stageId", sa.Integer(), nullable=False, server_default="99999"), + 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 ### @@ -79,4 +94,5 @@ def downgrade(): 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 ### diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 088f23f..a4d7047 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -24,6 +24,7 @@ mods: nameplates: False trophies: False character_icons: False + stages: False version: 11: diff --git a/titles/chuni/const.py b/titles/chuni/const.py index a5dd0a1..582d239 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -329,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): diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index aef8c1a..5745594 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -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) diff --git a/titles/chuni/img/stage/.gitignore b/titles/chuni/img/stage/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/titles/chuni/img/stage/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/titles/chuni/read.py b/titles/chuni/read.py index b880094..84432c9 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -69,6 +69,7 @@ class ChuniReader(BaseReader): 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") + 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"): @@ -569,6 +570,33 @@ class ChuniReader(BaseReader): 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 diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index da29da2..8bc719f 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -477,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, diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index d85d3f4..6db5374 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -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,13 +304,13 @@ 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 = Table( +linked_verse = Table( "chuni_static_linked_verse", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -325,7 +325,24 @@ linked_verse: Table = Table( Column("courseId4", Integer), Column("courseId5", Integer), UniqueConstraint( - "version", "linkedVerseId", name="chuni_static_linked_verse_pk" + "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", ) @@ -1307,3 +1324,54 @@ class ChuniStaticData(BaseData): 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() diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja index 417b04f..02a5c62 100644 --- a/titles/chuni/templates/chuni_index.jinja +++ b/titles/chuni/templates/chuni_index.jinja @@ -79,6 +79,12 @@
{{ system_voices[profile.voiceId]["name"] if system_voices|length > 0 else "Server DB needs upgraded or is not populated with necessary data" }}
Stage:
{{ stages[profile.stageId]["name"] if stages|length > 0 else "Server DB needs upgraded or is not populated with necessary data" }}
@@ -111,6 +117,21 @@ {% endif %} + {% if cur_version >= 18 %} + +

+
+ +
+ {% for item in stages.values() %} + {{ item[ + + {% endfor %} +
+
+
+ {% endif %} +
@@ -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) { From b3824f038f993d58cd6bc8e0d3d349b38f9419fc Mon Sep 17 00:00:00 2001 From: daydensteve Date: Thu, 8 Jan 2026 20:34:28 -0500 Subject: [PATCH 60/63] Fixed bad default subtrophy values (issue #235) --- .../318d52559e83_chuni_subtrophy_db_fix.py | 31 +++++++++++++++++++ titles/chuni/schema/profile.py | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 core/data/alembic/versions/318d52559e83_chuni_subtrophy_db_fix.py diff --git a/core/data/alembic/versions/318d52559e83_chuni_subtrophy_db_fix.py b/core/data/alembic/versions/318d52559e83_chuni_subtrophy_db_fix.py new file mode 100644 index 0000000..e42bc25 --- /dev/null +++ b/core/data/alembic/versions/318d52559e83_chuni_subtrophy_db_fix.py @@ -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 diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 8bc719f..1ab86a8 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -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), From 04019da9acf7c924e9aa335fcf93cd6b33d404ee Mon Sep 17 00:00:00 2001 From: Keeboy99 Date: Thu, 29 Jan 2026 11:43:10 +1300 Subject: [PATCH 61/63] importer fix + extras --- titles/chuni/index.py | 10 ++++++++-- titles/chuni/read.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index ce331f1..376ea85 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -121,7 +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(): @@ -306,8 +308,10 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif version >= 135 and version < 140: # LUMINOUS PLUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS - elif version >= 140: + 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 @@ -315,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 diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 84432c9..3c9513d 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -69,7 +69,8 @@ class ChuniReader(BaseReader): 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") - await self.read_stage(f"{dir}/stage", this_opt_id) + 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"): From 2d2b24739d3e650c030d84d5bb4d6369cdccf577 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 13 Feb 2026 21:37:08 -0500 Subject: [PATCH 62/63] mai2: pre-dx music reading --- titles/mai2/read.py | 92 +++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 1c86518..d9ffbca 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -53,6 +53,10 @@ class Mai2Reader(BaseReader): self.logger.error(f"tables directory not found in {self.bin_dir}") return + if not os.path.exists(f"{self.opt_dir}/tables"): + self.logger.warning(f"tables directory not found in {self.opt_dir}, not using") + self.opt_dir = None + if self.version >= Mai2Constants.VER_MAIMAI_MILK: if self.extra is None: self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag") @@ -63,41 +67,54 @@ class Mai2Reader(BaseReader): else: key = None - evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key) - txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key) - score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) + evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + jp_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + #en_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_ex.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + en_table = [] + score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + music_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmMusic.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + genre_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmGenre.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + + txt_table = {} # Build our translation table + for entry in jp_table: + vals = list(entry.values()) + txt_table[vals[0]] = ", ".join(vals[1:]) + for entry in en_table: + vals = list(entry.values()) + txt_table[vals[0]] = ", ".join(vals[1:]) + genre_lookup = {} + for entry in genre_table: + genre_lookup[entry['ID']] = txt_table[entry['名前テキスト']] + + self.logger.info(f"Insert {len(evt_table)} events") await self.read_old_events(evt_table) - await self.read_old_music(score_table, txt_table) - - if self.opt_dir is not None: - evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) - txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key) - score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) - - await self.read_old_events(evt_table) - await self.read_old_music(score_table, txt_table) - - return + self.logger.info(f"Insert {len(score_table)} charts") + await self.read_old_music(music_table, score_table, txt_table, genre_lookup) - def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]: - if not os.path.exists(f"{dir}/{file}"): + def load_table_raw(self, dir: str, file: str, key: Optional[bytes], opt_dir: Optional[str] = None) -> Optional[List[Dict[str, str]]]: + if opt_dir is not None and os.path.exists(f"{opt_dir}/{file}"): + fpath = f"{opt_dir}/{file}" + else: + fpath = f"{dir}/{file}" + + if not os.path.exists(fpath): self.logger.warning(f"file {file} does not exist in directory {dir}, skipping") return - self.logger.info(f"Load table {file} from {dir}") + self.logger.info(f"Load table {fpath}") if key is not None: cipher = AES.new(key, AES.MODE_CBC) - with open(f"{dir}/{file}", "rb") as f: + with open(fpath, "rb") as f: f_encrypted = f.read() f_data = cipher.decrypt(f_encrypted)[0x10:] else: - with open(f"{dir}/{file}", "rb") as f: + with open(fpath, "rb") as f: f_data = f.read()[0x10:] if f_data is None or not f_data: - self.logger.warning(f"file {dir} could not be read, skipping") + self.logger.warning(f"file {fpath} could not be read, skipping") return f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning @@ -325,20 +342,37 @@ class Mai2Reader(BaseReader): for event in events: evt_id = int(event.get('イベントID', '0')) - evt_expire_time = float(event.get('オフ時強制時期', '0.0')) - is_exp = bool(int(event.get('海外許可', '0'))) - is_aou = bool(int(event.get('AOU許可', '0'))) name = event.get('comment', f'evt_{evt_id}') await self.data.static.put_game_event(self.version, 0, evt_id, name) - - if not (is_exp or is_aou): - await self.data.static.toggle_game_event(self.version, evt_id, False) - async def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: - if scores is None or text is None: + async def read_old_music(self, music: Optional[List[Dict[str, str]]], scores: Optional[List[Dict[str, str]]], text: Optional[Dict[str, str]], genre: Dict[str, str]) -> None: + if music is None or scores is None or text is None: return - # TODO + + last_music = music[0] + for score in scores: + mid = score['ID'][:-2] + cid = score['ID'][-2:] + + if last_music['ID'] != mid: + for x in range(len(music)): + if music[x]['ID'] == mid: + last_music = music[x] + break + + await self.data.static.put_game_music( + self.version, + int(mid), + int(cid), + text[last_music['タイトル']], + text[last_music['アーティスト']], + genre[last_music['GenreID']], + last_music['BPM'], + last_music['Ver'], + float(score['LV']), + text[f"RST_SCORECREATOR_{int(score['譜面作者ID']):04d}"] + ) async def read_opt_info(self, directory: str) -> Optional[int]: datacfg_file = os.path.join(directory, "DataConfig.xml") From d3c978de5742de6d906691beca7d3eace5393c4f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 27 Feb 2026 19:46:34 -0500 Subject: [PATCH 63/63] mai2: fixup for pre-dx table reading --- titles/mai2/read.py | 90 +++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index d9ffbca..221d704 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -67,58 +67,34 @@ class Mai2Reader(BaseReader): else: key = None - evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - jp_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - #en_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_ex.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - en_table = [] - score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - music_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmMusic.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - genre_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmGenre.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + jp_table = self.parse_textout(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + en_table = self.parse_textout(f"{self.bin_dir}/tables", "mmtextout_ex.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + evt_table = self.parse_table(f"{self.bin_dir}/tables", "mmEvent.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + score_table = self.parse_table(f"{self.bin_dir}/tables", "mmScore.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + music_table = self.parse_table(f"{self.bin_dir}/tables", "mmMusic.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) + genre_table = self.parse_table(f"{self.bin_dir}/tables", "mmGenre.bin", key, f"{self.opt_dir}/tables" if self.opt_dir else None) - txt_table = {} # Build our translation table - for entry in jp_table: - vals = list(entry.values()) - txt_table[vals[0]] = ", ".join(vals[1:]) - for entry in en_table: - vals = list(entry.values()) - txt_table[vals[0]] = ", ".join(vals[1:]) - genre_lookup = {} for entry in genre_table: - genre_lookup[entry['ID']] = txt_table[entry['名前テキスト']] + genre_lookup[entry['ID']] = jp_table[entry['名前テキスト']] self.logger.info(f"Insert {len(evt_table)} events") await self.read_old_events(evt_table) self.logger.info(f"Insert {len(score_table)} charts") - await self.read_old_music(music_table, score_table, txt_table, genre_lookup) + await self.read_old_music(music_table, score_table, jp_table, genre_lookup) - def load_table_raw(self, dir: str, file: str, key: Optional[bytes], opt_dir: Optional[str] = None) -> Optional[List[Dict[str, str]]]: - if opt_dir is not None and os.path.exists(f"{opt_dir}/{file}"): - fpath = f"{opt_dir}/{file}" - else: - fpath = f"{dir}/{file}" + def parse_textout(self, dir: str, file: str, key: Optional[bytes], opt_dir: Optional[str] = None) -> Dict[str, str]: + f_decoded = self.load_table_raw(dir, file, key, opt_dir) + out = {} + for line in f_decoded.splitlines(): + matcher = re.match(r"^[A-Z]+\( L\"(.+)\" ,L\"(.*)\" \)$", line) + if not matcher: continue + out[matcher.group(1)] = matcher.group(2) - if not os.path.exists(fpath): - self.logger.warning(f"file {file} does not exist in directory {dir}, skipping") - return - - self.logger.info(f"Load table {fpath}") - if key is not None: - cipher = AES.new(key, AES.MODE_CBC) - with open(fpath, "rb") as f: - f_encrypted = f.read() - f_data = cipher.decrypt(f_encrypted)[0x10:] - - else: - with open(fpath, "rb") as f: - f_data = f.read()[0x10:] - - if f_data is None or not f_data: - self.logger.warning(f"file {fpath} could not be read, skipping") - return - - f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning - f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] + return out + + def parse_table(self, dir: str, file: str, key: Optional[bytes], opt_dir: Optional[str] = None) -> Optional[List[Dict[str, str]]]: + f_decoded = self.load_table_raw(dir, file, key, opt_dir) f_split = f_decoded.splitlines() has_struct_def = "struct " in f_decoded @@ -193,6 +169,34 @@ class Mai2Reader(BaseReader): self.logger.warning("Failed load table content, skipping") return + def load_table_raw(self, dir: str, file: str, key: Optional[bytes], opt_dir: Optional[str] = None) -> str: + if opt_dir is not None and os.path.exists(f"{opt_dir}/{file}"): + fpath = f"{opt_dir}/{file}" + else: + fpath = f"{dir}/{file}" + + if not os.path.exists(fpath): + self.logger.warning(f"file {file} does not exist in directory {dir}, skipping") + return + + self.logger.info(f"Load table {fpath}") + if key is not None: + cipher = AES.new(key, AES.MODE_CBC) + with open(fpath, "rb") as f: + f_encrypted = f.read() + f_data = cipher.decrypt(f_encrypted)[0x10:] + + else: + with open(fpath, "rb") as f: + f_data = f.read()[0x10:] + + if f_data is None or not f_data: + self.logger.warning(f"file {fpath} could not be read, skipping") + return + + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning + return codecs.utf_16_le_decode(f_data_deflate)[0] + async def get_events(self, base_dir: str, opt_id: int = None) -> None: self.logger.info(f"Reading events from {base_dir}...")