32 Commits

Author SHA1 Message Date
b1e629b3d7 [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: Hay1tsme/artemis#234
Co-authored-by: daydensteve <daydensteve@gmail.com>
Co-committed-by: daydensteve <daydensteve@gmail.com>
2025-10-18 15:25:23 +00:00
e11db14292 Merge pull request '[mai2] Prism Plus support' (#232) from SoulGateKey/artemis:prism_plus_support into develop
Reviewed-on: Hay1tsme/artemis#232
2025-10-07 17:59:48 +00:00
77152bf25c change down_revision when final merge 2025-10-07 17:59:02 +00:00
f346d8572d chuni: fix typo in upgrade script 2025-10-07 13:26:54 -04:00
21415de775 add lut 2025-09-19 23:13:57 +08:00
d16ebe27d9 Merge pull request 'develop' (#16) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#16
2025-09-18 13:05:46 +00:00
fa18b4c6a2 Merge pull request 'develop' (#15) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#15
2025-09-16 17:54:52 +00:00
46d79d156b Merge branch 'mai2_rival_support' into prism_plus_support 2025-08-01 00:55:12 +08:00
15e8eb535b Merge remote-tracking branch 'origin/prism_plus_support' into prism_plus_support 2025-07-28 20:06:24 +08:00
392fdb3783 Merge branch 'mai2_rival_support' into prism_plus_support 2025-07-28 01:04:00 +08:00
91545bb974 Merge branch 'mai2_rival_support' into prism_plus_support 2025-07-26 18:43:37 +08:00
e52362d87f Merge pull request 'develop' (#14) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#14
2025-07-25 18:16:21 +00:00
e3ec58b238 Merge branch 'mai2_MusicScoreApi_support' into prism_plus_support 2025-07-26 01:38:45 +08:00
0a4dc8dbb0 add Gate 7,8,9,10 support 2025-07-22 19:55:33 +08:00
180c027575 Sync Develop branch's update 2025-07-22 03:37:39 +08:00
611806828a add Gate 5,6 judgement 2025-05-15 08:19:42 +08:00
8b050e89eb fix conflict mark 2025-05-15 03:53:42 +08:00
394ec74fb7 add key unlock condition define 2025-05-15 03:14:24 +08:00
a2a333b13f Merge remote-tracking branch 'origin/prism_plus_support' into prism_plus_support
# Conflicts:
#	titles/mai2/index.py
2025-05-15 02:45:56 +08:00
933d8bea21 add import 2025-05-15 02:44:37 +08:00
f70af35343 Merge branch 'refs/heads/develop' into prism_plus_support
# Conflicts:
#	core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py
#	core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py
#	core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py
#	core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py
#	titles/mai2/index.py
#	titles/mai2/prism.py
#	titles/mai2/read.py
#	titles/mai2/schema/static.py
2025-05-15 02:41:55 +08:00
1e7f367d0f Merge pull request 'develop' (#9) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#9
2025-04-08 04:37:17 +00:00
134af15ed7 update readme.md and game_specific_info.md 2025-04-07 09:15:29 +08:00
a4bcca9171 add clientplaytimeapi support 2025-04-07 09:15:29 +08:00
d598c8fba0 add prism+ playlog support 2025-04-07 09:15:29 +08:00
3b7a577ea2 add prism+ consts and cm support 2025-04-07 09:15:29 +08:00
dd10508e68 unused database deleted 2025-04-04 09:12:08 +08:00
c0df7cd084 database support for prism
kaleidxScope Key Condition store
2025-04-04 09:10:41 +08:00
756c7ce951 update readme.md and game_specific_info.md 2025-04-02 13:56:56 +08:00
4ceac7db35 add clientplaytimeapi support 2025-04-02 13:49:46 +08:00
f8888c2392 add prism+ playlog support 2025-04-02 12:42:40 +08:00
9bc18f179d add prism+ consts and cm support 2025-04-02 12:37:43 +08:00
51 changed files with 295 additions and 66 deletions

View File

@ -1,7 +1,7 @@
"""CHUNITHM VERSE support """CHUNITHM VERSE support
Revision ID: 49c295e89cd4 Revision ID: 49c295e89cd4
Revises: f6007bbf057d Revises: 7070a6fa8cdc
Create Date: 2025-03-09 14:10:03.067328 Create Date: 2025-03-09 14:10:03.067328
""" """
@ -13,7 +13,7 @@ from sqlalchemy.sql import func
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = "49c295e89cd4" revision = "49c295e89cd4"
down_revision = "f6007bbf057d" down_revision = "7070a6fa8cdc"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -60,7 +60,7 @@ def upgrade():
sa.Column("conditionType", sa.Integer()), sa.Column("conditionType", sa.Integer()),
sa.Column("score", sa.Integer()), sa.Column("score", sa.Integer()),
sa.Column("life", 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( sa.UniqueConstraint(
"version", "version",
"user", "user",

View File

@ -10,6 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '5cf98cfe52ad' revision = '5cf98cfe52ad'
down_revision = '263884e774cc' down_revision = '263884e774cc'
branch_labels = None branch_labels = None

View File

@ -0,0 +1,29 @@
"""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 = '49c295e89cd4'
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 ###

View File

@ -205,32 +205,32 @@ Presents are items given to the user when they login, with a little animation (f
### Versions ### Versions
| Game Code | Version ID | Version Name | | Game Code | Version ID | Version Name |
|-----------|------------|-------------------------| |----------|------------|-------------------------|
| SBXL | 0 | maimai | | SBXL | 0 | maimai |
| SBXL | 1 | maimai PLUS | | SBXL | 1 | maimai PLUS |
| SBZF | 2 | maimai GreeN | | SBZF | 2 | maimai GreeN |
| SBZF | 3 | maimai GreeN PLUS | | SBZF | 3 | maimai GreeN PLUS |
| SDBM | 4 | maimai ORANGE | | SDBM | 4 | maimai ORANGE |
| SDBM | 5 | maimai ORANGE PLUS | | SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK | | SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS | | SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKi | | SDDK | 8 | maimai MURASAKi |
| SDDK | 9 | maimai MURASAKi PLUS | | SDDK | 9 | maimai MURASAKi PLUS |
| SDDZ | 10 | maimai MiLK | | SDDZ | 10 | maimai MiLK |
| SDDZ | 11 | maimai MiLK PLUS | | SDDZ | 11 | maimai MiLK PLUS |
| SDEY | 12 | maimai FiNALE | | SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX | | SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS | | SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash | | SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS | | SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX UNiVERSE | | SDEZ | 17 | maimai DX UNiVERSE |
| SDEZ | 18 | maimai DX UNiVERSE PLUS | | SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS | | SDEZ | 20 | maimai DX FESTiVAL PLUS |
| SDEZ | 21 | maimai DX BUDDiES | | SDEZ | 21 | maimai DX BUDDiES |
| SDEZ | 22 | maimai DX BUDDiES PLUS | | SDEZ | 22 | maimai DX BUDDiES PLUS |
| SDEZ | 23 | maimai DX PRiSM | | SDEZ | 23 | maimai DX PRiSM |
| SDEZ | 24 | maimai DX PRiSM PLUS |
### Importer ### Importer

View File

@ -83,6 +83,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ BUDDiES + BUDDiES
+ BUDDiES PLUS + BUDDiES PLUS
+ PRiSM + PRiSM
+ PRiSM PLUS
+ O.N.G.E.K.I. + O.N.G.E.K.I.
+ SUMMER + SUMMER

View File

@ -13,6 +13,7 @@ from core.config import CoreConfig
from .database import ChuniData from .database import ChuniData
from .config import ChuniConfig from .config import ChuniConfig
from .const import ChuniConstants, AvatarCategory, ItemKind from .const import ChuniConstants, AvatarCategory, ItemKind
from .read import ChuniReader
def pairwise(iterable): def pairwise(iterable):
@ -91,6 +92,9 @@ class ChuniFrontend(FE_Base):
self.data = ChuniData(cfg, self.game_cfg) self.data = ChuniData(cfg, self.game_cfg)
self.nav_name = "Chunithm" 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]: def get_routes(self) -> List[Route]:
return [ return [
Route("/", self.render_GET, methods=['GET']), Route("/", self.render_GET, methods=['GET']),
@ -252,12 +256,12 @@ class ChuniFrontend(FE_Base):
artist=music_chart.artist artist=music_chart.artist
title=music_chart.title title=music_chart.title
(jacket, ext) = path.splitext(music_chart.jacketPath) (jacket, ext) = path.splitext(music_chart.jacketPath)
jacket += ".png" jacket += ".webp"
else: else:
difficultyNum=0 difficultyNum=0
artist="unknown" artist="unknown"
title="musicid: " + str(record.musicId) 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 # 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) is_favorite = await self.data.item.is_favorite(user_id, version, record.musicId)
@ -313,12 +317,12 @@ class ChuniFrontend(FE_Base):
title=song.title title=song.title
genre=song.genre genre=song.genre
(jacket, ext) = path.splitext(song.jacketPath) (jacket, ext) = path.splitext(song.jacketPath)
jacket += ".png" jacket += ".webp"
else: else:
artist="unknown" artist="unknown"
title="musicid: " + str(favorite.favId) title="musicid: " + str(favorite.favId)
genre="unknown" genre="unknown"
jacket = "unknown.png" jacket = "unknown.webp"
# add a new collection for the genre if this is our first time seeing it # add a new collection for the genre if this is our first time seeing it
if genre not in favorites_by_genre: if genre not in favorites_by_genre:
@ -370,7 +374,7 @@ class ChuniFrontend(FE_Base):
item = dict() item = dict()
item["id"] = row["mapIconId"] item["id"] = row["mapIconId"]
item["name"] = row["name"] item["name"] = row["name"]
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png" item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp"
items[row["mapIconId"]] = item items[row["mapIconId"]] = item
return (items, len(rows)) return (items, len(rows))
@ -395,7 +399,7 @@ class ChuniFrontend(FE_Base):
item = dict() item = dict()
item["id"] = row["voiceId"] item["id"] = row["voiceId"]
item["name"] = row["name"] item["name"] = row["name"]
item["imagePath"] = path.splitext(row["imagePath"])[0] + ".png" item["imagePath"] = path.splitext(row["imagePath"])[0] + ".webp"
items[row["voiceId"]] = item items[row["voiceId"]] = item
return (items, len(rows)) return (items, len(rows))
@ -418,7 +422,7 @@ class ChuniFrontend(FE_Base):
item = dict() item = dict()
item["id"] = row["nameplateId"] item["id"] = row["nameplateId"]
item["name"] = row["name"] item["name"] = row["name"]
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png" item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp"
items[row["nameplateId"]] = item items[row["nameplateId"]] = item
return (items, len(rows)) return (items, len(rows))
@ -464,7 +468,7 @@ class ChuniFrontend(FE_Base):
item = dict() item = dict()
item["id"] = row["characterId"] item["id"] = row["characterId"]
item["name"] = row["name"] item["name"] = row["name"]
item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".png" item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".webp"
items[row["characterId"]] = item items[row["characterId"]] = item
return (items, len(rows)) return (items, len(rows))
@ -482,8 +486,8 @@ class ChuniFrontend(FE_Base):
item = dict() item = dict()
item["id"] = row["avatarAccessoryId"] item["id"] = row["avatarAccessoryId"]
item["name"] = row["name"] item["name"] = row["name"]
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png" item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp"
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png" item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp"
items[row["avatarAccessoryId"]] = item items[row["avatarAccessoryId"]] = item
return (items, len(rows)) return (items, len(rows))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,4 +2,4 @@
* *
# Except this file and default unknown # Except this file and default unknown
!.gitignore !.gitignore
!unknown.png !unknown.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,9 +1,11 @@
from logging import Logger
from typing import Optional from typing import Optional
from os import walk, path from os import walk, path, remove
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from read import BaseReader from read import BaseReader
from PIL import Image from PIL import Image
import configparser import configparser
import glob
from core.config import CoreConfig from core.config import CoreConfig
from titles.chuni.database import ChuniData from titles.chuni.database import ChuniData
@ -43,6 +45,9 @@ class ChuniReader(BaseReader):
if self.version >= ChuniConstants.VER_CHUNITHM_NEW: if self.version >= ChuniConstants.VER_CHUNITHM_NEW:
we_diff = "5" 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 # character images could be stored anywhere across all the data dirs. Map them first
self.logger.info(f"Mapping DDS image files...") self.logger.info(f"Mapping DDS image files...")
dds_images = dict() dds_images = dict()
@ -533,17 +538,40 @@ class ChuniReader(BaseReader):
self.logger.warning(f"Failed to unlock challenge {id}") self.logger.warning(f"Failed to unlock challenge {id}")
def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None: def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None:
# Convert the image to 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) file_src = path.join(src_dir, filename)
(basename, ext) = path.splitext(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): if path.exists(file_src) and not path.exists(file_dst):
try: try:
im = Image.open(file_src) im = Image.open(file_src)
im.save(file_dst) im.save(file_dst)
except Exception: 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: def map_dds_images(self, image_dict: dict, dds_dir: str) -> None:
for root, dirs, files in walk(dds_dir): for root, dirs, files in walk(dds_dir):

View File

@ -14,13 +14,13 @@
<table class="table-large table-rowdistinct"> <table class="table-large table-rowdistinct">
<caption align="top">AVATAR</caption> <caption align="top">AVATAR</caption>
<tr><td style="height:340px; width:50%" rowspan=8> <tr><td style="height:340px; width:50%" rowspan=8>
<img class="avatar-preview avatar-preview-platform" src="img/avatar-platform.png"> <img class="avatar-preview avatar-preview-platform" src="img/avatar-platform.webp">
<img id="preview1_back" class="avatar-preview avatar-preview-back" src=""> <img id="preview1_back" class="avatar-preview avatar-preview-back" src="">
<img id="preview1_skin" class="avatar-preview avatar-preview-skin-rightfoot" src=""> <img id="preview1_skin" class="avatar-preview avatar-preview-skin-rightfoot" src="">
<img id="preview2_skin" class="avatar-preview avatar-preview-skin-leftfoot" src=""> <img id="preview2_skin" class="avatar-preview avatar-preview-skin-leftfoot" src="">
<img id="preview3_skin" class="avatar-preview avatar-preview-skin-body" src=""> <img id="preview3_skin" class="avatar-preview avatar-preview-skin-body" src="">
<img id="preview1_wear" class="avatar-preview avatar-preview-wear" src=""> <img id="preview1_wear" class="avatar-preview avatar-preview-wear" src="">
<img class="avatar-preview avatar-preview-common" src="img/avatar-common.png"> <img class="avatar-preview avatar-preview-common" src="img/avatar-common.webp">
<img id="preview1_head" class="avatar-preview avatar-preview-head" src=""> <img id="preview1_head" class="avatar-preview avatar-preview-head" src="">
<img id="preview1_face" class="avatar-preview avatar-preview-face" src=""> <img id="preview1_face" class="avatar-preview avatar-preview-face" src="">
<img id="preview1_item" class="avatar-preview avatar-preview-item-righthand" src=""> <img id="preview1_item" class="avatar-preview avatar-preview-item-righthand" src="">
@ -36,7 +36,7 @@
<tr><td>Front:</td><td><div id="name_front"></div></td></tr> <tr><td>Front:</td><td><div id="name_front"></div></td></tr>
<tr><td>Back:</td><td><div id="name_back"></div></td></tr> <tr><td>Back:</td><td><div id="name_back"></div></td></tr>
<tr><td colspan=3 style="padding:8px 0px; text-align: center;"> <tr><td colspan=3 style="padding:8px 0px; text-align: center; position: relative;">
<button id="save-btn" class="btn btn-primary" style="width:140px;" onClick="saveAvatar()">SAVE</button>&nbsp;&nbsp;&nbsp;&nbsp; <button id="save-btn" class="btn btn-primary" style="width:140px;" onClick="saveAvatar()">SAVE</button>&nbsp;&nbsp;&nbsp;&nbsp;
<button id="reset-btn" class="btn btn-danger" style="width:140px;" onClick="resetAvatar()">RESET</button> <button id="reset-btn" class="btn btn-danger" style="width:140px;" onClick="resetAvatar()">RESET</button>
</td></tr> </td></tr>

View File

@ -18,7 +18,7 @@
<img id="preview_nameplate" class="userbox userbox-nameplate" src=""> <img id="preview_nameplate" class="userbox userbox-nameplate" src="">
<!-- TEAM --> <!-- TEAM -->
<img class="userbox userbox-teamframe" src="img/rank/team3.png"> <img class="userbox userbox-teamframe" src="img/rank/team3.webp">
<div class="userbox userbox-teamname">{{team_name}}</div> <div class="userbox userbox-teamname">{{team_name}}</div>
<!-- TROPHY/TITLE --> <!-- TROPHY/TITLE -->
@ -26,7 +26,7 @@
<div id="preview_trophy_name" class="userbox userbox-trophy userbox-trophy-name"></div> <div id="preview_trophy_name" class="userbox userbox-trophy userbox-trophy-name"></div>
<!-- NAME/RATING --> <!-- NAME/RATING -->
<img class="userbox userbox-ratingframe" src="img/rank/rating0.png"> <img class="userbox userbox-ratingframe" src="img/rank/rating0.webp">
<div class="userbox userbox-name"> <div class="userbox userbox-name">
<span class="userbox-name-level-label">Lv.</span> <span class="userbox-name-level-label">Lv.</span>
{{ profile.level }}&nbsp;&nbsp;&nbsp;{{ profile.userName }} {{ profile.level }}&nbsp;&nbsp;&nbsp;{{ profile.userName }}
@ -37,7 +37,7 @@
</div> </div>
<!-- CHARACTER --> <!-- CHARACTER -->
<img class="userbox userbox-charaframe" src="img/character-bg.png"> <img class="userbox userbox-charaframe" src="img/character-bg.webp">
<img id="preview_character" class="userbox userbox-chara" src=""> <img id="preview_character" class="userbox userbox-chara" src="">
</td></tr> </td></tr>
@ -50,7 +50,7 @@
{% endfor %} {% endfor %}
</select> </select>
</div></td></tr> </div></td></tr>
{% if cur_version >= 17 %} <!-- SubTrophies introduced in VERSE -->
<tr><td>Trophy Sub 1:</td><td><div id="name_trophy"> <tr><td>Trophy Sub 1:</td><td><div id="name_trophy">
<select name="trophy-sub-1" id="trophy-sub-1" onclick="changeTrophySub1()" style="width:100%;"> <select name="trophy-sub-1" id="trophy-sub-1" onclick="changeTrophySub1()" style="width:100%;">
<option value="-1"></option> <option value="-1"></option>
@ -68,7 +68,8 @@
{% endfor %} {% endfor %}
</select> </select>
</div></td></tr> </div></td></tr>
{% endif %}
<tr><td>Character:</td><td><div id="name_character"></div></td></tr> <tr><td>Character:</td><td><div id="name_character"></div></td></tr>
<tr><td colspan=2 style="padding:8px 0px; text-align: center;"> <tr><td colspan=2 style="padding:8px 0px; text-align: center;">
@ -180,10 +181,10 @@ function changeItem(type, id, name, img) {
function getRankImage(selected_rank) { function getRankImage(selected_rank) {
for (const x of Array(12).keys()) { for (const x of Array(12).keys()) {
if (selected_rank.classList.contains("trophy-rank" + x.toString())) { 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() { function changeTrophy() {

View File

@ -208,7 +208,8 @@ class CardMakerReader(BaseReader):
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
"1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES,
"1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS, "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): for root, dirs, files in os.walk(base_dir):

View File

@ -61,6 +61,7 @@ class Mai2Constants:
VER_MAIMAI_DX_BUDDIES = 21 VER_MAIMAI_DX_BUDDIES = 21
VER_MAIMAI_DX_BUDDIES_PLUS = 22 VER_MAIMAI_DX_BUDDIES_PLUS = 22
VER_MAIMAI_DX_PRISM = 23 VER_MAIMAI_DX_PRISM = 23
VER_MAIMAI_DX_PRISM_PLUS = 24
VERSION_STRING = ( VERSION_STRING = (
"maimai", "maimai",
@ -86,7 +87,8 @@ class Mai2Constants:
"maimai DX FESTiVAL PLUS", "maimai DX FESTiVAL PLUS",
"maimai DX BUDDiES", "maimai DX BUDDiES",
"maimai DX BUDDiES PLUS", "maimai DX BUDDiES PLUS",
"maimai DX PRiSM" "maimai DX PRiSM",
"maimai DX PRiSM PLUS"
) )
KALEIDXSCOPE_KEY_CONDITION={ KALEIDXSCOPE_KEY_CONDITION={
1: [11009, 11008, 11100, 11097, 11098, 11099, 11163, 11162, 11161, 11228, 11229, 11231, 11463, 11464, 11465, 11538, 11539, 11541, 11620, 11622, 11623, 11737, 11738, 11164, 11230, 11466, 11540, 11621, 11739], 1: [11009, 11008, 11100, 11097, 11098, 11099, 11163, 11162, 11161, 11228, 11229, 11231, 11463, 11464, 11465, 11538, 11539, 11541, 11620, 11622, 11623, 11737, 11738, 11164, 11230, 11466, 11540, 11621, 11739],
@ -94,9 +96,21 @@ class Mai2Constants:
2: [11102, 11234, 11300, 11529, 11542, 11612], 2: [11102, 11234, 11300, 11529, 11542, 11612],
#白の扉: set Frame as "Latent Kingdom" (459504), play 3 or 4 songs by the composer 大国奏音 in 1 pc #白の扉: set Frame as "Latent Kingdom" (459504), play 3 or 4 songs by the composer 大国奏音 in 1 pc
3: [], 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], 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
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 = { MAI_VERSION_LUT = {
"100": VER_MAIMAI, "100": VER_MAIMAI,
@ -125,7 +139,8 @@ class Mai2Constants:
"135": VER_MAIMAI_DX_FESTIVAL_PLUS, "135": VER_MAIMAI_DX_FESTIVAL_PLUS,
"140": VER_MAIMAI_DX_BUDDIES, "140": VER_MAIMAI_DX_BUDDIES,
"145": VER_MAIMAI_DX_BUDDIES_PLUS, "145": VER_MAIMAI_DX_BUDDIES_PLUS,
"150": VER_MAIMAI_DX_PRISM "150": VER_MAIMAI_DX_PRISM,
"155": VER_MAIMAI_DX_PRISM_PLUS
} }
@classmethod @classmethod

View File

@ -32,6 +32,7 @@ from .festivalplus import Mai2FestivalPlus
from .buddies import Mai2Buddies from .buddies import Mai2Buddies
from .buddiesplus import Mai2BuddiesPlus from .buddiesplus import Mai2BuddiesPlus
from .prism import Mai2Prism from .prism import Mai2Prism
from .prismplus import Mai2PrismPlus
class Mai2Servlet(BaseServlet): class Mai2Servlet(BaseServlet):
@ -68,7 +69,8 @@ class Mai2Servlet(BaseServlet):
Mai2FestivalPlus, Mai2FestivalPlus,
Mai2Buddies, Mai2Buddies,
Mai2BuddiesPlus, Mai2BuddiesPlus,
Mai2Prism Mai2Prism,
Mai2PrismPlus
] ]
self.logger = logging.getLogger("mai2") self.logger = logging.getLogger("mai2")
@ -195,7 +197,7 @@ class Mai2Servlet(BaseServlet):
if proto == "" or proto == "https://": if proto == "" or proto == "https://":
t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" 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 "" t_port = f":{title_port_int}" if title_port_int != 80 else ""
return ( return (
@ -343,10 +345,12 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
elif version >= 140 and version < 145: # BUDDiES elif version >= 140 and version < 145: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_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 internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
elif version >= 150: # PRiSM elif version >= 150 and version < 155:
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
elif version >= 155:
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
elif game_code == "SDGA": # Int elif game_code == "SDGA": # Int
if version < 105: # 1.0 if version < 105: # 1.0
@ -367,10 +371,12 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
elif version >= 140 and version < 145: # BUDDiES elif version >= 140 and version < 145: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_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 internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
elif version >= 150: # PRiSM elif version >= 150 and version < 155:
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
elif version >= 155:
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
elif game_code == "SDGB": # Chn elif game_code == "SDGB": # Chn
if version < 110: # Muji if version < 110: # Muji
@ -386,6 +392,7 @@ class Mai2Servlet(BaseServlet):
elif version >= 150: # PRiSM elif version >= 150: # PRiSM
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
if game_code == "SDGA": if game_code == "SDGA":

141
titles/mai2/prismplus.py Normal file
View File

@ -0,0 +1,141 @@
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
async def handle_upsert_client_play_time_api_request(self, data: Dict) -> Dict:
return{
"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},
{"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,11):
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)
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:
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
}

View File

@ -148,7 +148,8 @@ playlog = Table(
Column("extNum2", Integer), Column("extNum2", Integer),
Column("extNum4", Integer), Column("extNum4", Integer),
Column("extBool1", Boolean), # new with buddies Column("extBool1", Boolean), # new with buddies
Column("extBool2", Boolean), # new with prism Column("extBool2", Boolean), # new with prism IsRandomSelect
Column("extBool3", Boolean), # new with prism+ IsTrackSkip
Column("trialPlayAchievement", Integer), Column("trialPlayAchievement", Integer),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )