Merge pull request 'develop' (#9) from develop into prism_plus_support

Reviewed-on: https://gitea.tendokyu.moe/SoulGateKey/artemis/pulls/9
This commit is contained in:
SoulGateKey
2025-04-08 04:37:17 +00:00
19 changed files with 634 additions and 105 deletions

View File

@@ -0,0 +1,164 @@
"""acc_opt_tables
Revision ID: 263884e774cc
Revises: 1d0014d35220
Create Date: 2025-04-07 18:05:53.349320
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '263884e774cc'
down_revision = '1d0014d35220'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chuni_static_opt',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=False),
sa.Column('name', sa.VARCHAR(length=4), nullable=False),
sa.Column('sequence', sa.INTEGER(), nullable=False),
sa.Column('whenRead', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('isEnable', sa.BOOLEAN(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'name', name='chuni_static_opt_uk'),
mysql_charset='utf8mb4'
)
op.create_table('cm_static_opts',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=False),
sa.Column('name', sa.VARCHAR(length=4), nullable=False),
sa.Column('sequence', sa.INTEGER(), nullable=True),
sa.Column('gekiVersion', sa.INTEGER(), nullable=True),
sa.Column('gekiReleaseVer', sa.INTEGER(), nullable=True),
sa.Column('maiVersion', sa.INTEGER(), nullable=True),
sa.Column('maiReleaseVer', sa.INTEGER(), nullable=True),
sa.Column('whenRead', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('isEnable', sa.BOOLEAN(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'name', name='cm_static_opts_uk'),
mysql_charset='utf8mb4'
)
op.create_table('mai2_static_opt',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=False),
sa.Column('name', sa.VARCHAR(length=4), nullable=False),
sa.Column('sequence', sa.INTEGER(), nullable=False),
sa.Column('cmReleaseVer', sa.INTEGER(), nullable=False),
sa.Column('whenRead', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('isEnable', sa.BOOLEAN(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'name', name='mai2_static_opt_uk'),
mysql_charset='utf8mb4'
)
op.create_table('ongeki_static_opt',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=False),
sa.Column('name', sa.VARCHAR(length=4), nullable=False),
sa.Column('sequence', sa.INTEGER(), nullable=False),
sa.Column('cmReleaseVer', sa.INTEGER(), nullable=False),
sa.Column('whenRead', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('isEnable', sa.BOOLEAN(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'name', name='ongeki_static_opt_uk'),
mysql_charset='utf8mb4'
)
op.add_column('chuni_static_avatar', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_avatar', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_cards', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_cards', 'cm_static_opts', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_character', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_character', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_charge', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_charge', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_events', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_events', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_gachas', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_gachas', 'cm_static_opts', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_login_bonus', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_login_bonus', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_login_bonus_preset', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_login_bonus_preset', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_map_icon', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_map_icon', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_music', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_music', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_system_voice', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_system_voice', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('chuni_static_trophy', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'chuni_static_trophy', 'chuni_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('mai2_static_cards', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'mai2_static_cards', 'cm_static_opts', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('mai2_static_event', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'mai2_static_event', 'mai2_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('mai2_static_music', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'mai2_static_music', 'mai2_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('mai2_static_ticket', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'mai2_static_ticket', 'mai2_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('ongeki_static_cards', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'ongeki_static_cards', 'ongeki_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('ongeki_static_events', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'ongeki_static_events', 'ongeki_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('ongeki_static_gachas', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'ongeki_static_gachas', 'cm_static_opts', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('ongeki_static_music', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'ongeki_static_music', 'ongeki_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
op.add_column('ongeki_static_rewards', sa.Column('opt', sa.BIGINT(), nullable=True))
op.create_foreign_key(None, 'ongeki_static_rewards', 'ongeki_static_opt', ['opt'], ['id'], onupdate='cascade', ondelete='SET NULL')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("ongeki_static_rewards_ibfk_1", 'ongeki_static_rewards', type_='foreignkey')
op.drop_column('ongeki_static_rewards', 'opt')
op.drop_constraint("ongeki_static_music_ibfk_1", 'ongeki_static_music', type_='foreignkey')
op.drop_column('ongeki_static_music', 'opt')
op.drop_constraint("ongeki_static_gachas_ibfk_1", 'ongeki_static_gachas', type_='foreignkey')
op.drop_column('ongeki_static_gachas', 'opt')
op.drop_constraint("ongeki_static_events_ibfk_1", "ongeki_static_events", type_='foreignkey')
op.drop_column('ongeki_static_events', 'opt')
op.drop_constraint("ongeki_static_cards_ibfk_1", "ongeki_static_cards", type_='foreignkey')
op.drop_column('ongeki_static_cards', 'opt')
op.drop_constraint("mai2_static_ticket_ibfk_1", "mai2_static_ticket", type_='foreignkey')
op.drop_column('mai2_static_ticket', 'opt')
op.drop_constraint("mai2_static_music_ibfk_1", "mai2_static_music", type_='foreignkey')
op.drop_column('mai2_static_music', 'opt')
op.drop_constraint("mai2_static_event_ibfk_1", "mai2_static_event", type_='foreignkey')
op.drop_column('mai2_static_event', 'opt')
op.drop_constraint("mai2_static_cards_ibfk_1", "mai2_static_cards", type_='foreignkey')
op.drop_column('mai2_static_cards', 'opt')
op.drop_constraint("chuni_static_trophy_ibfk_1", "chuni_static_trophy", type_='foreignkey')
op.drop_column('chuni_static_trophy', 'opt')
op.drop_constraint("chuni_static_system_voice_ibfk_1", "chuni_static_system_voice", type_='foreignkey')
op.drop_column('chuni_static_system_voice', 'opt')
op.drop_constraint("chuni_static_music_ibfk_1", "chuni_static_music", type_='foreignkey')
op.drop_column('chuni_static_music', 'opt')
op.drop_constraint("chuni_static_map_icon_ibfk_1", "chuni_static_map_icon", type_='foreignkey')
op.drop_column('chuni_static_map_icon', 'opt')
op.drop_constraint("chuni_static_login_bonus_preset_ibfk_1", "chuni_static_login_bonus_preset", type_='foreignkey')
op.drop_column('chuni_static_login_bonus_preset', 'opt')
op.drop_constraint("chuni_static_login_bonus_ibfk_2", "chuni_static_login_bonus", type_='foreignkey')
op.drop_column('chuni_static_login_bonus', 'opt')
op.drop_constraint("chuni_static_gachas_ibfk_1", "chuni_static_gachas", type_='foreignkey')
op.drop_column('chuni_static_gachas', 'opt')
op.drop_constraint("chuni_static_events_ibfk_1", "chuni_static_events", type_='foreignkey')
op.drop_column('chuni_static_events', 'opt')
op.drop_constraint("chuni_static_charge_ibfk_1", "chuni_static_charge", type_='foreignkey')
op.drop_column('chuni_static_charge', 'opt')
op.drop_constraint("chuni_static_character_ibfk_1", "chuni_static_character", type_='foreignkey')
op.drop_column('chuni_static_character', 'opt')
op.drop_constraint("chuni_static_cards_ibfk_1", "chuni_static_cards", type_='foreignkey')
op.drop_column('chuni_static_cards', 'opt')
op.drop_constraint("chuni_static_avatar_ibfk_1", "chuni_static_avatar", type_='foreignkey')
op.drop_column('chuni_static_avatar', 'opt')
op.drop_table('ongeki_static_opt')
op.drop_table('mai2_static_opt')
op.drop_table('cm_static_opts')
op.drop_table('chuni_static_opt')
# ### end Alembic commands ###

View File

@@ -1,8 +1,8 @@
"""Mai2 Kaleidx Scope Support """Mai2 PRiSM support
Revision ID: 16f34bf7b968 Revision ID: 5cf98cfe52ad
Revises: d0f1c7fa9505 Revises: 263884e774cc
Create Date: 2025-04-02 07:06:15.829591 Create Date: 2025-04-08 08:00:51.243089
""" """
from alembic import op from alembic import op
@@ -10,15 +10,15 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '16f34bf7b968' revision = '5cf98cfe52ad'
down_revision = 'd0f1c7fa9505' down_revision = '263884e774cc'
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('mai2_score_kaleidx_scope', op.create_table('mai2_score_kaleidxscope',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user', sa.Integer(), nullable=False), sa.Column('user', sa.Integer(), nullable=False),
sa.Column('gateId', sa.Integer(), nullable=True), sa.Column('gateId', sa.Integer(), nullable=True),
@@ -41,10 +41,12 @@ def upgrade():
sa.UniqueConstraint('user', 'gateId', name='mai2_score_best_uk'), sa.UniqueConstraint('user', 'gateId', name='mai2_score_best_uk'),
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )
op.add_column('mai2_playlog', sa.Column('extBool2', sa.Boolean(), nullable=True, server_default=sa.text("NULL")))
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### 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')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@@ -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 ###

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timezone
from os import walk from os import walk
from types import ModuleType from types import ModuleType
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import math
import jwt import jwt
from starlette.requests import Request from starlette.requests import Request
@@ -92,6 +93,8 @@ class Utils:
return cls.real_title_port_ssl return cls.real_title_port_ssl
def floor_to_nearest_005(version: int) -> int:
return (version // 5) * 5
def create_sega_auth_key( def create_sega_auth_key(
aime_id: int, aime_id: int,

View File

@@ -195,10 +195,10 @@ Config file is located in `config/cxb.yaml`.
### Presents ### Presents
Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add: Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add:
| Game Version | Item ID | Item Kind | Item Description | Present Description | | Game Version | Item ID | Item Kind | Item Description | Present Description |
|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------| |--------------|---------|----------------------|--------------------------------------------|----------------------------------------------------------------------------|
| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial | | BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial number, for project raputa |
| | | | | number, for project raputa | | PRiSM (23) | 3 | KaleidxScopeKey (15) | 紫の鍵 (Purple Key) | Officially obtained on the webui with a serial number, for KaleidxScope |
### Versions ### Versions

View File

@@ -1,5 +1,6 @@
from enum import Enum, IntEnum from enum import Enum, IntEnum
from typing import Optional
from core.utils import floor_to_nearest_005
class ChuniConstants: class ChuniConstants:
GAME_CODE = "SDBT" GAME_CODE = "SDBT"
@@ -78,10 +79,34 @@ class ChuniConstants:
( 0, "D"), ( 0, "D"),
] ]
VERSION_LUT = {
"100": VER_CHUNITHM,
"105": VER_CHUNITHM_PLUS,
"110": VER_CHUNITHM_AIR,
"115": VER_CHUNITHM_AIR_PLUS,
"120": VER_CHUNITHM_STAR,
"125": VER_CHUNITHM_STAR_PLUS,
"130": VER_CHUNITHM_AMAZON,
"135": VER_CHUNITHM_AMAZON_PLUS,
"140": VER_CHUNITHM_CRYSTAL,
"145": VER_CHUNITHM_CRYSTAL_PLUS,
"150": VER_CHUNITHM_PARADISE,
"200": VER_CHUNITHM_NEW,
"205": VER_CHUNITHM_NEW_PLUS,
"210": VER_CHUNITHM_SUN,
"215": VER_CHUNITHM_SUN_PLUS,
"220": VER_CHUNITHM_LUMINOUS,
"225": VER_CHUNITHM_LUMINOUS_PLUS,
}
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver] return cls.VERSION_NAMES[ver]
@classmethod
def int_ver_to_game_ver(cls, ver: int) -> Optional[int]:
""" Takes an int ver (ex 100 for 1.00) and returns an internal game version """
return cls.VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)
class MapAreaConditionType(IntEnum): class MapAreaConditionType(IntEnum):
"""Condition types for the GetGameMapAreaConditionApi endpoint. Incomplete. """Condition types for the GetGameMapAreaConditionApi endpoint. Incomplete.

View File

@@ -457,7 +457,7 @@ class ChuniFrontend(FE_Base):
user_characters = [] user_characters = []
if not force_unlocked: if not force_unlocked:
user_characters = await self.data.item.get_characters(profile.user) user_characters = await self.data.item.get_characters(profile.user)
user_characters = [chara["characterId"] for chara in user_characters] + [profile.characterId, profile.charaIllustId] user_characters = [chara["characterId"] for chara in user_characters] + [profile.characterId]
for row in rows: for row in rows:
if force_unlocked or row["defaultHave"] or row["characterId"] in user_characters: if force_unlocked or row["defaultHave"] or row["characterId"] in user_characters:

View File

@@ -3,6 +3,7 @@ from os import walk, path
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
from core.config import CoreConfig from core.config import CoreConfig
from titles.chuni.database import ChuniData from titles.chuni.database import ChuniData
@@ -50,18 +51,19 @@ class ChuniReader(BaseReader):
for dir in data_dirs: for dir in data_dirs:
self.logger.info(f"Read from {dir}") self.logger.info(f"Read from {dir}")
await self.read_events(f"{dir}/event") this_opt_id = await self.read_opt_info(dir) # this also treats A000 as an opt, which is intended
await self.read_music(f"{dir}/music", we_diff) await self.read_events(f"{dir}/event", this_opt_id)
await self.read_charges(f"{dir}/chargeItem") await self.read_music(f"{dir}/music", we_diff, this_opt_id)
await self.read_avatar(f"{dir}/avatarAccessory") await self.read_charges(f"{dir}/chargeItem", this_opt_id)
await self.read_login_bonus(f"{dir}/") await self.read_avatar(f"{dir}/avatarAccessory", this_opt_id)
await self.read_nameplate(f"{dir}/namePlate") await self.read_login_bonus(f"{dir}/", this_opt_id)
await self.read_trophy(f"{dir}/trophy") await self.read_nameplate(f"{dir}/namePlate", this_opt_id)
await self.read_character(f"{dir}/chara", dds_images) await self.read_trophy(f"{dir}/trophy", this_opt_id)
await self.read_map_icon(f"{dir}/mapIcon") await self.read_character(f"{dir}/chara", dds_images, this_opt_id)
await self.read_system_voice(f"{dir}/systemVoice") await self.read_map_icon(f"{dir}/mapIcon", this_opt_id)
await self.read_system_voice(f"{dir}/systemVoice", this_opt_id)
async def read_login_bonus(self, root_dir: str) -> None: async def read_login_bonus(self, root_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
@@ -132,7 +134,7 @@ class ChuniReader(BaseReader):
f"Failed to insert login bonus {bonus_id}" f"Failed to insert login bonus {bonus_id}"
) )
async def read_events(self, evt_dir: str) -> None: async def read_events(self, evt_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(evt_dir): for root, dirs, files in walk(evt_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/Event.xml"): if path.exists(f"{root}/{dir}/Event.xml"):
@@ -154,7 +156,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert event {id}") self.logger.warning(f"Failed to insert event {id}")
async def read_music(self, music_dir: str, we_diff: str = "4") -> None: async def read_music(self, music_dir: str, we_diff: str = "4", opt_id: Optional[int] = None) -> None:
max_title_len = MusicTable.columns["title"].type.length max_title_len = MusicTable.columns["title"].type.length
max_artist_len = MusicTable.columns["artist"].type.length max_artist_len = MusicTable.columns["artist"].type.length
@@ -230,7 +232,7 @@ class ChuniReader(BaseReader):
f"Failed to insert music {song_id} chart {chart_id}" f"Failed to insert music {song_id} chart {chart_id}"
) )
async def read_charges(self, charge_dir: str) -> None: async def read_charges(self, charge_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(charge_dir): for root, dirs, files in walk(charge_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/ChargeItem.xml"): if path.exists(f"{root}/{dir}/ChargeItem.xml"):
@@ -259,7 +261,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert charge {id}") self.logger.warning(f"Failed to insert charge {id}")
async def read_avatar(self, avatar_dir: str) -> None: async def read_avatar(self, avatar_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(avatar_dir): for root, dirs, files in walk(avatar_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
@@ -292,7 +294,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert avatarAccessory {id}") self.logger.warning(f"Failed to insert avatarAccessory {id}")
async def read_nameplate(self, nameplate_dir: str) -> None: async def read_nameplate(self, nameplate_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(nameplate_dir): for root, dirs, files in walk(nameplate_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/NamePlate.xml"): if path.exists(f"{root}/{dir}/NamePlate.xml"):
@@ -303,7 +305,7 @@ class ChuniReader(BaseReader):
for name in xml_root.findall("name"): for name in xml_root.findall("name"):
id = name.find("id").text id = name.find("id").text
name = name.find("str").text name = name.find("str").text
sortName = xml_root.find("sortName").text sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
defaultHave = xml_root.find("defaultHave").text == 'true' defaultHave = xml_root.find("defaultHave").text == 'true'
disableFlag = xml_root.find("disableFlag") # may not exist in older data disableFlag = xml_root.find("disableFlag") # may not exist in older data
is_enabled = True if (disableFlag is None or disableFlag.text == "false") else False is_enabled = True if (disableFlag is None or disableFlag.text == "false") else False
@@ -321,7 +323,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert nameplate {id}") self.logger.warning(f"Failed to insert nameplate {id}")
async def read_trophy(self, trophy_dir: str) -> None: async def read_trophy(self, trophy_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(trophy_dir): for root, dirs, files in walk(trophy_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/Trophy.xml"): if path.exists(f"{root}/{dir}/Trophy.xml"):
@@ -346,18 +348,21 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert trophy {id}") self.logger.warning(f"Failed to insert trophy {id}")
async def read_character(self, chara_dir: str, dds_images: dict) -> None: async def read_character(self, chara_dir: str, dds_images: dict, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(chara_dir): for root, dirs, files in walk(chara_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/Chara.xml"): if path.exists(f"{root}/{dir}/Chara.xml"):
with open(f"{root}/{dir}/Chara.xml", "r", encoding='utf-8') as fp: with open(f"{root}/{dir}/Chara.xml", "r", encoding='utf-8') as fp:
strdata = fp.read() strdata = fp.read()
# ET may choke if there is a & symbol (which is present in some character xml)
if "&" in strdata:
strdata = strdata.replace("&", "&")
xml_root = ET.fromstring(strdata) xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"): for name in xml_root.findall("name"):
id = name.find("id").text id = name.find("id").text
name = name.find("str").text name = name.find("str").text
sortName = xml_root.find("sortName").text sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
for work in xml_root.findall("works"): for work in xml_root.findall("works"):
worksName = work.find("str").text worksName = work.find("str").text
rareType = xml_root.find("rareType").text rareType = xml_root.find("rareType").text
@@ -390,7 +395,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert character {id}") self.logger.warning(f"Failed to insert character {id}")
async def read_map_icon(self, mapicon_dir: str) -> None: async def read_map_icon(self, mapicon_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(mapicon_dir): for root, dirs, files in walk(mapicon_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/MapIcon.xml"): if path.exists(f"{root}/{dir}/MapIcon.xml"):
@@ -401,7 +406,7 @@ class ChuniReader(BaseReader):
for name in xml_root.findall("name"): for name in xml_root.findall("name"):
id = name.find("id").text id = name.find("id").text
name = name.find("str").text name = name.find("str").text
sortName = xml_root.find("sortName").text sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
for image in xml_root.findall("image"): for image in xml_root.findall("image"):
iconPath = image.find("path").text iconPath = image.find("path").text
self.copy_image(iconPath, f"{root}/{dir}", "titles/chuni/img/mapIcon/") self.copy_image(iconPath, f"{root}/{dir}", "titles/chuni/img/mapIcon/")
@@ -418,7 +423,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to map icon {id}") self.logger.warning(f"Failed to map icon {id}")
async def read_system_voice(self, voice_dir: str) -> None: async def read_system_voice(self, voice_dir: str, opt_id: Optional[int] = None) -> None:
for root, dirs, files in walk(voice_dir): for root, dirs, files in walk(voice_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/SystemVoice.xml"): if path.exists(f"{root}/{dir}/SystemVoice.xml"):
@@ -429,7 +434,7 @@ class ChuniReader(BaseReader):
for name in xml_root.findall("name"): for name in xml_root.findall("name"):
id = name.find("id").text id = name.find("id").text
name = name.find("str").text name = name.find("str").text
sortName = xml_root.find("sortName").text sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
for image in xml_root.findall("image"): for image in xml_root.findall("image"):
imagePath = image.find("path").text imagePath = image.find("path").text
self.copy_image(imagePath, f"{root}/{dir}", "titles/chuni/img/systemVoice/") self.copy_image(imagePath, f"{root}/{dir}", "titles/chuni/img/systemVoice/")
@@ -446,6 +451,49 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to system voice {id}") self.logger.warning(f"Failed to system voice {id}")
async def read_opt_info(self, directory: str) -> Optional[int]:
if not path.exists(f"{directory}/data.conf"):
self.logger.warning(f"{directory} does not contain data.conf, opt info will not be read")
return None
data_config = configparser.ConfigParser()
if not data_config.read(f"{directory}/data.conf", 'utf-8'):
self.logger.warning(f"{directory}/data.conf failed to read or parse, opt info will not be read")
return None
if 'Version' not in data_config:
self.logger.warning(f"{directory}/data.conf contains no Version section, opt info will not be read")
return None
if 'Name' not in data_config['Version']: # Probably not worth checking that the other sections exist
self.logger.warning(f"{directory}/data.conf contains no Name item in the Version section, opt info will not be read")
return None
if 'VerMajor' not in data_config['Version']: # Probably not worth checking that the other sections exist
self.logger.warning(f"{directory}/data.conf contains no VerMajor item in the Version section, opt info will not be read")
return None
if 'VerMinor' not in data_config['Version']: # Probably not worth checking that the other sections exist
self.logger.warning(f"{directory}/data.conf contains no VerMinor item in the Version section, opt info will not be read")
return None
if 'VerRelease' not in data_config['Version']: # Probably not worth checking that the other sections exist
self.logger.warning(f"{directory}/data.conf contains no VerRelease item in the Version section, opt info will not be read")
return None
opt_seq = data_config['Version']['VerRelease']
opt_folder = path.basename(path.normpath(directory))
opt_id = await self.data.static.get_opt_by_version_folder(self.version, opt_folder)
if not opt_id:
opt_id = await self.data.static.put_opt(self.version, opt_folder, opt_seq)
if not opt_id:
self.logger.error(f"Failed to put opt folder info for {opt_folder}")
return None
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
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 png so we can easily display it in the frontend
file_src = path.join(src_dir, filename) file_src = path.join(src_dir, filename)

View File

@@ -7,8 +7,7 @@ from sqlalchemy import (
PrimaryKeyConstraint, PrimaryKeyConstraint,
and_, and_,
) )
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, BIGINT, Float, INTEGER, VARCHAR, BOOLEAN
from sqlalchemy.engine.base import Connection
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
@@ -17,6 +16,19 @@ from datetime import datetime
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
opts = Table(
"chuni_static_opt",
metadata,
Column("id", BIGINT, primary_key=True, nullable=False),
Column("version", INTEGER, nullable=False),
Column("name", VARCHAR(4), nullable=False), # Axxx
Column("sequence", INTEGER, nullable=False), # VerRelease in data.conf
Column("whenRead", TIMESTAMP, nullable=False, server_default=func.now()),
Column("isEnable", BOOLEAN, nullable=False, server_default="1"),
UniqueConstraint("version", "name", name="chuni_static_opt_uk"),
mysql_charset="utf8mb4",
)
events = Table( events = Table(
"chuni_static_events", "chuni_static_events",
metadata, metadata,
@@ -27,6 +39,7 @@ events = Table(
Column("name", String(255)), Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()), Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -44,6 +57,7 @@ music = Table(
Column("genre", String(255)), Column("genre", String(255)),
Column("jacketPath", String(255)), Column("jacketPath", String(255)),
Column("worldsEndTag", String(7)), Column("worldsEndTag", String(7)),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"), UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -59,6 +73,7 @@ charge = Table(
Column("consumeType", Integer), Column("consumeType", Integer),
Column("sellingAppeal", Boolean), Column("sellingAppeal", Boolean),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"), UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -76,6 +91,7 @@ avatar = Table(
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("defaultHave", Boolean, server_default="0"), Column("defaultHave", Boolean, server_default="0"),
Column("sortName", String(255)), Column("sortName", String(255)),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"), UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -110,6 +126,7 @@ character = Table(
Column("imagePath3", String(255)), Column("imagePath3", String(255)),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("defaultHave", Boolean, server_default="0"), Column("defaultHave", Boolean, server_default="0"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "characterId", name="chuni_static_character_uk"), UniqueConstraint("version", "characterId", name="chuni_static_character_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -124,6 +141,7 @@ trophy = Table(
Column("rareType", Integer), Column("rareType", Integer),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("defaultHave", Boolean, server_default="0"), Column("defaultHave", Boolean, server_default="0"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "trophyId", name="chuni_static_trophy_uk"), UniqueConstraint("version", "trophyId", name="chuni_static_trophy_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -139,6 +157,7 @@ map_icon = Table(
Column("iconPath", String(255)), Column("iconPath", String(255)),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("defaultHave", Boolean, server_default="0"), Column("defaultHave", Boolean, server_default="0"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "mapIconId", name="chuni_static_mapicon_uk"), UniqueConstraint("version", "mapIconId", name="chuni_static_mapicon_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -154,6 +173,7 @@ system_voice = Table(
Column("imagePath", String(255)), Column("imagePath", String(255)),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("defaultHave", Boolean, server_default="0"), Column("defaultHave", Boolean, server_default="0"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "voiceId", name="chuni_static_systemvoice_uk"), UniqueConstraint("version", "voiceId", name="chuni_static_systemvoice_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -175,6 +195,7 @@ gachas = Table(
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"), UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -195,6 +216,7 @@ cards = Table(
Column("combo", Integer, nullable=False), Column("combo", Integer, nullable=False),
Column("chain", Integer, nullable=False), Column("chain", Integer, nullable=False),
Column("skillName", String(255), nullable=False), Column("skillName", String(255), nullable=False),
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"), UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -219,6 +241,7 @@ login_bonus_preset = Table(
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column("presetName", String(255), nullable=False), Column("presetName", String(255), nullable=False),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
PrimaryKeyConstraint( PrimaryKeyConstraint(
"presetId", "version", name="chuni_static_login_bonus_preset_pk" "presetId", "version", name="chuni_static_login_bonus_preset_pk"
), ),
@@ -238,6 +261,7 @@ login_bonus = Table(
Column("itemNum", Integer, nullable=False), Column("itemNum", Integer, nullable=False),
Column("needLoginDayCount", Integer, nullable=False), Column("needLoginDayCount", Integer, nullable=False),
Column("loginBonusCategoryType", Integer, nullable=False), Column("loginBonusCategoryType", Integer, nullable=False),
Column("opt", BIGINT),
UniqueConstraint( UniqueConstraint(
"version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk"
), ),
@@ -251,10 +275,18 @@ login_bonus = Table(
ondelete="CASCADE", ondelete="CASCADE",
name="chuni_static_login_bonus_ibfk_1", name="chuni_static_login_bonus_ibfk_1",
), ),
ForeignKeyConstraint(
["opt"],
[
"chuni_static_opt.id",
],
onupdate="SET NULL",
ondelete="CASCADE",
name="chuni_static_login_bonus_ibfk_2",
),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
class ChuniStaticData(BaseData): class ChuniStaticData(BaseData):
async def put_login_bonus( async def put_login_bonus(
self, self,
@@ -327,17 +359,17 @@ class ChuniStaticData(BaseData):
return result.fetchone() return result.fetchone()
async def put_login_bonus_preset( async def put_login_bonus_preset(
self, version: int, preset_id: int, preset_name: str, is_enabled: bool self, version: int, preset_id: int, preset_name: str, isEnabled: bool
) -> Optional[int]: ) -> Optional[int]:
sql = insert(login_bonus_preset).values( sql = insert(login_bonus_preset).values(
presetId=preset_id, presetId=preset_id,
version=version, version=version,
presetName=preset_name, presetName=preset_name,
isEnabled=is_enabled, isEnabled=isEnabled,
) )
conflict = sql.on_duplicate_key_update( conflict = sql.on_duplicate_key_update(
presetName=preset_name, isEnabled=is_enabled presetName=preset_name, isEnabled=isEnabled
) )
result = await self.execute(conflict) result = await self.execute(conflict)
@@ -346,12 +378,12 @@ class ChuniStaticData(BaseData):
return result.lastrowid return result.lastrowid
async def get_login_bonus_presets( async def get_login_bonus_presets(
self, version: int, is_enabled: bool = True self, version: int, isEnabled: bool = True
) -> Optional[List[Row]]: ) -> Optional[List[Row]]:
sql = login_bonus_preset.select( sql = login_bonus_preset.select(
and_( and_(
login_bonus_preset.c.version == version, login_bonus_preset.c.version == version,
login_bonus_preset.c.isEnabled == is_enabled, login_bonus_preset.c.isEnabled == isEnabled,
) )
) )
@@ -542,7 +574,6 @@ class ChuniStaticData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
async def put_avatar( async def put_avatar(
self, self,
version: int, version: int,
@@ -926,4 +957,86 @@ class ChuniStaticData(BaseData):
result = await self.execute(sql) result = await self.execute(sql)
if result is None: if result is None:
return None return None
return result.fetchone() return result.fetchone()
async def put_opt(self, version: int, folder: str, sequence: int) -> Optional[int]:
sql = insert(opts).values(version=version, name=folder, sequence=sequence)
conflict = sql.on_duplicate_key_update(sequence=sequence, whenRead=datetime.now())
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"Failed to insert opt! version {version} folder {folder} sequence {sequence}")
return None
return result.lastrowid
async def get_opt_by_version_folder(self, version: int, folder: str) -> Optional[Row]:
result = await self.execute(opts.select(and_(
opts.c.version == version,
opts.c.name == folder,
)))
if result is None:
return None
return result.fetchone()
async def get_opt_by_version_sequence(self, version: int, sequence: str) -> Optional[Row]:
result = await self.execute(opts.select(and_(
opts.c.version == version,
opts.c.sequence == sequence,
)))
if result is None:
return None
return result.fetchone()
async def get_opts_by_version(self, version: int) -> Optional[List[Row]]:
result = await self.execute(opts.select(opts.c.version == version))
if result is None:
return None
return result.fetchall()
async def get_opts_enabled_by_version(self, version: int) -> Optional[List[Row]]:
result = await self.execute(opts.select(and_(
opts.c.version == version,
opts.c.isEnable == True,
)))
if result is None:
return None
return result.fetchall()
async def get_latest_enabled_opt_by_version(self, version: int) -> Optional[Row]:
result = await self.execute(
opts.select(and_(
opts.c.version == version,
opts.c.isEnable == True,
)).order_by(opts.c.sequence.desc())
)
if result is None:
return None
return result.fetchone()
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 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))
if result is None:
self.logger.error(f"Failed to set opt enabled status to {enabled} for opt {opt_id}")
return False
return True

View File

@@ -118,9 +118,9 @@ userbox_components = {
"{{ nameplates[profile.nameplateId]["texturePath"] }}", "", "", ""], "{{ nameplates[profile.nameplateId]["texturePath"] }}", "", "", ""],
"character":["{{ characters|length }}", "character":["{{ characters|length }}",
"{{ profile.charaIllustId }}", "{{ profile.characterId }}",
"{{ characters[profile.charaIllustId]["name"] }}", "{{ characters[profile.characterId]["name"] }}",
"{{ characters[profile.charaIllustId]["iconPath"] }}", "", "", ""] "{{ characters[profile.characterId]["iconPath"] }}", "", "", ""]
}; };
types = Object.keys(userbox_components); types = Object.keys(userbox_components);
orig_trophy = curr_trophy = "{{ profile.trophyId }}"; orig_trophy = curr_trophy = "{{ profile.trophyId }}";

View File

@@ -327,3 +327,39 @@ class CardMakerReader(BaseReader):
maxSelectPoint=max_select_point, maxSelectPoint=max_select_point,
) )
self.logger.info(f"Added ongeki gacha {gacha_id}") self.logger.info(f"Added ongeki gacha {gacha_id}")
async def read_opt(self, base_dir: str) -> None:
self.logger.info(f"Reading opt data from {base_dir}...")
cm_data_cfg = None
cm_data_cfg_file = os.path.join(base_dir, "DataConfig.xml")
geki_data_cfg = None
geki_data_cfg_file = os.path.join(base_dir, "GEKI", "DataConfig.xml")
mai2_data_cfg = None
mai2_data_cfg_file = os.path.join(base_dir, "MAI", "DataConfig.xml")
if os.path.exists(cm_data_cfg_file):
with open(cm_data_cfg_file, "r") as f:
cm_data_cfg = ET.fromstring(f.read())
else:
self.logger.info(f"No DataConfig.xml in {base_dir}, sequence will be null")
if os.path.exists(geki_data_cfg_file):
with open(geki_data_cfg_file, "r") as f:
geki_data_cfg = ET.fromstring(f.read())
else:
self.logger.info(f"Cannot find {geki_data_cfg_file}, gekiVersion and gekiReleaseVer will be null")
if os.path.exists(mai2_data_cfg_file):
with open(mai2_data_cfg_file, "r") as f:
mai2_data_cfg = ET.fromstring(f.read())
else:
self.logger.info(f"Cannot find {mai2_data_cfg_file}, mai2Version and mai2ReleaseVer will be null")
cm_rel_ver = int(cm_data_cfg.find("DataConfig/version/release").text)
geki_rel_ver = int(geki_data_cfg.find("DataConfig/version/release").text)
mai2_rel_ver = int(mai2_data_cfg.find("DataConfig/version/release").text)
mai2_db_ver = Mai2Constants.int_ver_to_game_ver(mai2_data_cfg.find("DataConfig/version/major").text + mai2_data_cfg.find("DataConfig/version/minor").text)

View File

@@ -1,3 +1,6 @@
from typing import Optional
from core.utils import floor_to_nearest_005
class Mai2Constants: class Mai2Constants:
GRADE = { GRADE = {
"D": 0, "D": 0,
@@ -86,7 +89,57 @@ class Mai2Constants:
"maimai DX PRiSM", "maimai DX PRiSM",
"maimai DX PRiSM PLUS" "maimai DX PRiSM PLUS"
) )
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],
#青の扉: Played 29 songs
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
4: [11023, 11106, 11221, 11222, 11300, 11374, 11458, 11523, 11619, 11663, 11746],
#青の扉: Played 11 songs
}
MAI_VERSION_LUT = {
"100": VER_MAIMAI,
"110": VER_MAIMAI_PLUS,
"120": VER_MAIMAI_GREEN,
"130": VER_MAIMAI_GREEN_PLUS,
"140": VER_MAIMAI_ORANGE,
"150": VER_MAIMAI_ORANGE_PLUS,
"160": VER_MAIMAI_PINK,
"170": VER_MAIMAI_PINK_PLUS,
"180": VER_MAIMAI_MURASAKI,
"185": VER_MAIMAI_MURASAKI_PLUS,
"190": VER_MAIMAI_MILK,
"195": VER_MAIMAI_MILK_PLUS,
"197": VER_MAIMAI_FINALE,
}
MAI2_VERSION_LUT = {
"100": VER_MAIMAI_DX,
"105": VER_MAIMAI_DX_PLUS,
"110": VER_MAIMAI_DX_SPLASH,
"115": VER_MAIMAI_DX_SPLASH_PLUS,
"120": VER_MAIMAI_DX_UNIVERSE,
"125": VER_MAIMAI_DX_UNIVERSE_PLUS,
"130": VER_MAIMAI_DX_FESTIVAL,
"135": VER_MAIMAI_DX_FESTIVAL_PLUS,
"140": VER_MAIMAI_DX_BUDDIES,
"145": VER_MAIMAI_DX_BUDDIES_PLUS,
"150": VER_MAIMAI_DX_PRISM
}
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
""" Takes an internal game version (ex 13 for maimai DX) and returns a the full name of the version """
return cls.VERSION_STRING[ver] return cls.VERSION_STRING[ver]
@classmethod
def int_ver_to_game_ver(cls, ver: int, is_dx = True) -> Optional[int]:
""" Takes an int ver (ex 100 for 1.00) and returns an internal game version """
if is_dx:
return cls.MAI2_VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)
else:
if ver >= 197:
return cls.VER_MAIMAI_FINALE
return cls.MAI_VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)

View File

@@ -284,8 +284,8 @@ class Mai2DX(Mai2Base):
# added in PRiSM # added in PRiSM
if "userKaleidxScopeList" in upsert and len(upsert["userKaleidxScopeList"]) > 0: if "userKaleidxScopeList" in upsert and len(upsert["userKaleidxScopeList"]) > 0:
for kaleidx_scope in upsert["userKaleidxScopeList"]: for kaleidxscope in upsert["userKaleidxScopeList"]:
await self.data.score.put_user_kaleidx_scope(user_id, kaleidx_scope) await self.data.score.put_user_kaleidxscope(user_id, kaleidxscope)
return {"returnCode": 1, "apiName": "UpsertUserAllApi"} return {"returnCode": 1, "apiName": "UpsertUserAllApi"}

View File

@@ -4,7 +4,6 @@ from core.config import CoreConfig
from titles.mai2.buddiesplus import Mai2BuddiesPlus from titles.mai2.buddiesplus import Mai2BuddiesPlus
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
from titles.mai2.schema.score import kaleidx_scope
class Mai2Prism(Mai2BuddiesPlus): class Mai2Prism(Mai2BuddiesPlus):
@@ -44,24 +43,65 @@ class Mai2Prism(Mai2BuddiesPlus):
{"gateId": 2, "phaseId": 6}, {"gateId": 2, "phaseId": 6},
{"gateId": 3, "phaseId": 6}, {"gateId": 3, "phaseId": 6},
{"gateId": 4, "phaseId": 6}, {"gateId": 4, "phaseId": 6},
{"gateId": 5, "phaseId": 6},
{"gateId": 6, "phaseId": 6}
] ]
} }
async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict: async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict:
kaleidx_scope = await self.data.score.get_user_kaleidx_scope_list(data["userId"]) # kaleidxscope keyget condition judgement
# player may get key before GateFound
for gate in range(1,5):
if gate == 1 or gate == 4:
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)
if kaleidx_scope is None: 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)
kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"])
if kaleidxscope is None:
return {"userId": data["userId"], "userKaleidxScopeList":[]} return {"userId": data["userId"], "userKaleidxScopeList":[]}
kaleidx_scope_list = [] kaleidxscope_list = []
for kaleidx_scope_data in kaleidx_scope: for kaleidxscope_data in kaleidxscope:
tmp = kaleidx_scope_data._asdict() tmp = kaleidxscope_data._asdict()
tmp.pop("user") tmp.pop("user")
tmp.pop("id") tmp.pop("id")
kaleidx_scope_list.append(tmp) kaleidxscope_list.append(tmp)
return { return {
"userId": data["userId"], "userId": data["userId"],
"userKaleidxScopeList": kaleidx_scope_list "userKaleidxScopeList": kaleidxscope_list
} }

View File

@@ -728,10 +728,11 @@ class Mai2ItemData(BaseData):
# Do an anti-join with the mai2_item_item table to exclude any # Do an anti-join with the mai2_item_item table to exclude any
# items the users have already owned. # items the users have already owned.
if exclude_owned: if exclude_owned:
sql = sql.join( sql = sql.outerjoin(
item, item,
(present.c.itemKind == item.c.itemKind) (present.c.itemKind == item.c.itemKind)
& (present.c.itemId == item.c.itemId) & (present.c.itemId == item.c.itemId)
& (item.c.user == user_id)
) )
condition &= (item.c.itemKind.is_(None) & item.c.itemId.is_(None)) condition &= (item.c.itemKind.is_(None) & item.c.itemId.is_(None))

View File

@@ -176,8 +176,8 @@ playlog_2p = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
kaleidx_scope = Table( kaleidxscope = Table(
"mai2_score_kaleidx_scope", "mai2_score_kaleidxscope",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column( Column(
@@ -482,21 +482,21 @@ class Mai2ScoreData(BaseData):
return None return None
return result.scalar() return result.scalar()
async def get_user_kaleidx_scope_list(self, user_id: int) -> Optional[List[Row]]: async def get_user_kaleidxscope_list(self, user_id: int) -> Optional[List[Row]]:
sql = kaleidx_scope.select(kaleidx_scope.c.user == user_id) sql = kaleidxscope.select(kaleidxscope.c.user == user_id)
result = await self.execute(sql) result = await self.execute(sql)
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()
async def put_user_kaleidx_scope(self, user_id: int, user_kaleidx_scope_data: Dict) -> Optional[int]: async def put_user_kaleidxscope(self, user_id: int, user_kaleidxscope_data: Dict) -> Optional[int]:
user_kaleidx_scope_data["user"] = user_id user_kaleidxscope_data["user"] = user_id
sql = insert(kaleidx_scope).values(**user_kaleidx_scope_data) sql = insert(kaleidxscope).values(**user_kaleidxscope_data)
conflict = sql.on_duplicate_key_update(**user_kaleidx_scope_data) conflict = sql.on_duplicate_key_update(**user_kaleidxscope_data)
result = await self.execute(conflict) result = await self.execute(conflict)
if result is None: if result is None:
self.logger.error(f"put_user_kaleidx_scope: Failed to insert! user_id {user_id}") self.logger.error(f"put_user_kaleidxscope: Failed to insert! user_id {user_id}")
return None return None
return result.lastrowid return result.lastrowid

View File

@@ -2,13 +2,27 @@ from core.data.schema.base import BaseData, metadata
from typing import Optional, Dict, List from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, BIGINT, Float, INTEGER, BOOLEAN, VARCHAR
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from datetime import datetime from datetime import datetime
opts = Table(
"mai2_static_opt",
metadata,
Column("id", BIGINT, primary_key=True, nullable=False),
Column("version", INTEGER, nullable=False),
Column("name", VARCHAR(4), nullable=False), # Axxx
Column("sequence", INTEGER, nullable=False), # release in DataConfig.xml
Column("cmReleaseVer", INTEGER, nullable=False),
Column("whenRead", TIMESTAMP, nullable=False, server_default=func.now()),
Column("isEnable", BOOLEAN, nullable=False, server_default="1"),
UniqueConstraint("version", "name", name="mai2_static_opt_uk"),
mysql_charset="utf8mb4",
)
event = Table( event = Table(
"mai2_static_event", "mai2_static_event",
metadata, metadata,
@@ -19,6 +33,7 @@ event = Table(
Column("name", String(255)), Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()), Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("mai2_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -37,6 +52,7 @@ music = Table(
Column("addedVersion", String(255)), Column("addedVersion", String(255)),
Column("difficulty", Float), Column("difficulty", Float),
Column("noteDesigner", String(255)), Column("noteDesigner", String(255)),
Column("opt", ForeignKey("mai2_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"), UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -51,6 +67,7 @@ ticket = Table(
Column("name", String(255)), Column("name", String(255)),
Column("price", Integer, server_default="1"), Column("price", Integer, server_default="1"),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("mai2_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"), UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -67,6 +84,7 @@ cards = Table(
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"), UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )

View File

@@ -1,6 +1,6 @@
from typing import Final, Dict from typing import Optional
from enum import Enum from enum import Enum
from core.utils import floor_to_nearest_005
class OngekiConstants: class OngekiConstants:
GAME_CODE = "SDDT" GAME_CODE = "SDDT"
@@ -106,6 +106,24 @@ class OngekiConstants:
"O.N.G.E.K.I. bright MEMORY Act.3", "O.N.G.E.K.I. bright MEMORY Act.3",
) )
VERSION_LUT = {
"100": VER_ONGEKI,
"105": VER_ONGEKI_PLUS,
"110": VER_ONGEKI_SUMMER,
"115": VER_ONGEKI_SUMMER_PLUS,
"120": VER_ONGEKI_RED,
"125": VER_ONGEKI_RED_PLUS,
"130": VER_ONGEKI_BRIGHT,
"135": VER_ONGEKI_BRIGHT_MEMORY,
"140": VER_ONGEKI_BRIGHT_MEMORY,
"145": VER_ONGEKI_BRIGHT_MEMORY_ACT3,
}
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver] return cls.VERSION_NAMES[ver]
@classmethod
def int_ver_to_game_ver(cls, ver: int) -> Optional[int]:
""" Takes an int ver (ex 100 for 1.00) and returns an internal game version """
return cls.VERSION_LUT.get(str(floor_to_nearest_005(ver)), None)

View File

@@ -1,6 +1,6 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, BIGINT, Float, INTEGER, VARCHAR, BOOLEAN
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
@@ -9,6 +9,37 @@ from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
from core.data.schema.arcade import machine from core.data.schema.arcade import machine
opts = Table(
"ongeki_static_opt",
metadata,
Column("id", BIGINT, primary_key=True, nullable=False),
Column("version", INTEGER, nullable=False),
Column("name", VARCHAR(4), nullable=False), # Axxx
Column("sequence", INTEGER, nullable=False), # release in DataConfig.xml
Column("cmReleaseVer", INTEGER, nullable=False),
Column("whenRead", TIMESTAMP, nullable=False, server_default=func.now()),
Column("isEnable", BOOLEAN, nullable=False, server_default="1"),
UniqueConstraint("version", "name", name="ongeki_static_opt_uk"),
mysql_charset="utf8mb4",
)
cm_opts = Table(
"cm_static_opts",
metadata,
Column("id", BIGINT, primary_key=True, nullable=False),
Column("version", INTEGER, nullable=False),
Column("name", VARCHAR(4), nullable=False), # Axxx
Column("sequence", INTEGER), # Not all opts have a DataConfig.xml
Column("gekiVersion", INTEGER), # GEKI/DataConfig.xml
Column("gekiReleaseVer", INTEGER), # GEKI/DataConfig.xml
Column("maiVersion", INTEGER), # MAI/DataConfig.xml
Column("maiReleaseVer", INTEGER), # MAI/DataConfig.xml
Column("whenRead", TIMESTAMP, nullable=False, server_default=func.now()),
Column("isEnable", BOOLEAN, nullable=False, server_default="1"),
UniqueConstraint("version", "name", name="cm_static_opts_uk"),
mysql_charset="utf8mb4",
)
events = Table( events = Table(
"ongeki_static_events", "ongeki_static_events",
metadata, metadata,
@@ -20,6 +51,7 @@ events = Table(
Column("startDate", TIMESTAMP, server_default=func.now()), Column("startDate", TIMESTAMP, server_default=func.now()),
Column("endDate", TIMESTAMP, server_default=func.now()), Column("endDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -36,6 +68,7 @@ music = Table(
Column("artist", String(255)), Column("artist", String(255)),
Column("genre", String(255)), Column("genre", String(255)),
Column("level", Float), Column("level", Float),
Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"), UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -59,6 +92,7 @@ gachas = Table(
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"), UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -94,6 +128,7 @@ cards = Table(
Column("skillId", Integer, nullable=False), Column("skillId", Integer, nullable=False),
Column("choKaikaSkillId", Integer, nullable=False), Column("choKaikaSkillId", Integer, nullable=False),
Column("cardNumber", String(255)), Column("cardNumber", String(255)),
Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -107,6 +142,7 @@ rewards = Table(
Column("rewardname", String(255), nullable=False), Column("rewardname", String(255), nullable=False),
Column("itemKind", Integer, nullable=False), Column("itemKind", Integer, nullable=False),
Column("itemId", Integer, nullable=False), Column("itemId", Integer, nullable=False),
Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
UniqueConstraint("version", "rewardId", name="ongeki_static_rewards_uk"), UniqueConstraint("version", "rewardId", name="ongeki_static_rewards_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )