diff --git a/core/data/alembic/versions/263884e774cc_acc_opt_tables.py b/core/data/alembic/versions/263884e774cc_acc_opt_tables.py new file mode 100644 index 0000000..1e24813 --- /dev/null +++ b/core/data/alembic/versions/263884e774cc_acc_opt_tables.py @@ -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 ### diff --git a/core/utils.py b/core/utils.py index af96451..92f9bf5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone from os import walk from types import ModuleType from typing import Any, Dict, Optional +import math import jwt from starlette.requests import Request @@ -92,6 +93,8 @@ class Utils: return cls.real_title_port_ssl +def floor_to_nearest_005(version: int) -> int: + return (version // 5) * 5 def create_sega_auth_key( aime_id: int, diff --git a/titles/chuni/const.py b/titles/chuni/const.py index d0d73d5..fd05003 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -1,5 +1,6 @@ from enum import Enum, IntEnum - +from typing import Optional +from core.utils import floor_to_nearest_005 class ChuniConstants: GAME_CODE = "SDBT" @@ -78,10 +79,34 @@ class ChuniConstants: ( 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 def game_ver_to_string(cls, ver: int): 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): """Condition types for the GetGameMapAreaConditionApi endpoint. Incomplete. diff --git a/titles/chuni/read.py b/titles/chuni/read.py index b25d97f..fe0c411 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -3,6 +3,7 @@ from os import walk, path import xml.etree.ElementTree as ET from read import BaseReader from PIL import Image +import configparser from core.config import CoreConfig from titles.chuni.database import ChuniData @@ -50,18 +51,19 @@ class ChuniReader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") - await self.read_events(f"{dir}/event") - await self.read_music(f"{dir}/music", we_diff) - await self.read_charges(f"{dir}/chargeItem") - await self.read_avatar(f"{dir}/avatarAccessory") - await self.read_login_bonus(f"{dir}/") - await self.read_nameplate(f"{dir}/namePlate") - await self.read_trophy(f"{dir}/trophy") - await self.read_character(f"{dir}/chara", dds_images) - await self.read_map_icon(f"{dir}/mapIcon") - await self.read_system_voice(f"{dir}/systemVoice") + this_opt_id = await self.read_opt_info(dir) # this also treats A000 as an opt, which is intended + await self.read_events(f"{dir}/event", this_opt_id) + await self.read_music(f"{dir}/music", we_diff, this_opt_id) + await self.read_charges(f"{dir}/chargeItem", this_opt_id) + await self.read_avatar(f"{dir}/avatarAccessory", this_opt_id) + await self.read_login_bonus(f"{dir}/", this_opt_id) + await self.read_nameplate(f"{dir}/namePlate", this_opt_id) + await self.read_trophy(f"{dir}/trophy", this_opt_id) + await self.read_character(f"{dir}/chara", dds_images, this_opt_id) + await self.read_map_icon(f"{dir}/mapIcon", this_opt_id) + await self.read_system_voice(f"{dir}/systemVoice", this_opt_id) - 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 dir in dirs: if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): @@ -132,7 +134,7 @@ class ChuniReader(BaseReader): 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 dir in dirs: if path.exists(f"{root}/{dir}/Event.xml"): @@ -154,7 +156,7 @@ class ChuniReader(BaseReader): else: 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_artist_len = MusicTable.columns["artist"].type.length @@ -230,7 +232,7 @@ class ChuniReader(BaseReader): 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 dir in dirs: if path.exists(f"{root}/{dir}/ChargeItem.xml"): @@ -259,7 +261,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): @@ -292,7 +294,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/NamePlate.xml"): @@ -321,7 +323,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/Trophy.xml"): @@ -346,7 +348,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/Chara.xml"): @@ -390,7 +392,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/MapIcon.xml"): @@ -418,7 +420,7 @@ class ChuniReader(BaseReader): else: 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 dir in dirs: if path.exists(f"{root}/{dir}/SystemVoice.xml"): @@ -446,6 +448,49 @@ class ChuniReader(BaseReader): else: 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: # Convert the image to png so we can easily display it in the frontend file_src = path.join(src_dir, filename) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index e3070ec..0f7dc4a 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -7,8 +7,7 @@ from sqlalchemy import ( PrimaryKeyConstraint, and_, ) -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float -from sqlalchemy.engine.base import Connection +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, BIGINT, Float, INTEGER, VARCHAR, BOOLEAN from sqlalchemy.engine import Row from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select @@ -17,6 +16,19 @@ from datetime import datetime 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( "chuni_static_events", metadata, @@ -27,6 +39,7 @@ events = Table( Column("name", String(255)), Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), mysql_charset="utf8mb4", ) @@ -44,6 +57,7 @@ music = Table( Column("genre", String(255)), Column("jacketPath", String(255)), Column("worldsEndTag", String(7)), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"), mysql_charset="utf8mb4", ) @@ -59,6 +73,7 @@ charge = Table( Column("consumeType", Integer), Column("sellingAppeal", Boolean), Column("enabled", Boolean, server_default="1"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"), mysql_charset="utf8mb4", ) @@ -76,6 +91,7 @@ avatar = Table( Column("isEnabled", Boolean, server_default="1"), Column("defaultHave", Boolean, server_default="0"), Column("sortName", String(255)), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"), mysql_charset="utf8mb4", ) @@ -110,6 +126,7 @@ character = Table( Column("imagePath3", String(255)), Column("isEnabled", Boolean, server_default="1"), Column("defaultHave", Boolean, server_default="0"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "characterId", name="chuni_static_character_uk"), mysql_charset="utf8mb4", ) @@ -124,6 +141,7 @@ trophy = Table( Column("rareType", Integer), Column("isEnabled", Boolean, server_default="1"), Column("defaultHave", Boolean, server_default="0"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "trophyId", name="chuni_static_trophy_uk"), mysql_charset="utf8mb4", ) @@ -139,6 +157,7 @@ map_icon = Table( Column("iconPath", String(255)), Column("isEnabled", Boolean, server_default="1"), Column("defaultHave", Boolean, server_default="0"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "mapIconId", name="chuni_static_mapicon_uk"), mysql_charset="utf8mb4", ) @@ -154,6 +173,7 @@ system_voice = Table( Column("imagePath", String(255)), Column("isEnabled", Boolean, server_default="1"), Column("defaultHave", Boolean, server_default="0"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "voiceId", name="chuni_static_systemvoice_uk"), mysql_charset="utf8mb4", ) @@ -175,6 +195,7 @@ gachas = Table( Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"), mysql_charset="utf8mb4", ) @@ -195,6 +216,7 @@ cards = Table( Column("combo", Integer, nullable=False), Column("chain", Integer, nullable=False), Column("skillName", String(255), nullable=False), + Column("opt", ForeignKey("cm_static_opts.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"), mysql_charset="utf8mb4", ) @@ -219,6 +241,7 @@ login_bonus_preset = Table( Column("version", Integer, nullable=False), Column("presetName", String(255), nullable=False), Column("isEnabled", Boolean, server_default="1"), + Column("opt", ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")), PrimaryKeyConstraint( "presetId", "version", name="chuni_static_login_bonus_preset_pk" ), @@ -238,6 +261,7 @@ login_bonus = Table( Column("itemNum", Integer, nullable=False), Column("needLoginDayCount", Integer, nullable=False), Column("loginBonusCategoryType", Integer, nullable=False), + Column("opt", BIGINT), UniqueConstraint( "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" ), @@ -251,10 +275,18 @@ login_bonus = Table( ondelete="CASCADE", 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", ) - class ChuniStaticData(BaseData): async def put_login_bonus( self, @@ -327,17 +359,17 @@ class ChuniStaticData(BaseData): return result.fetchone() 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]: sql = insert(login_bonus_preset).values( presetId=preset_id, version=version, presetName=preset_name, - isEnabled=is_enabled, + isEnabled=isEnabled, ) conflict = sql.on_duplicate_key_update( - presetName=preset_name, isEnabled=is_enabled + presetName=preset_name, isEnabled=isEnabled ) result = await self.execute(conflict) @@ -346,12 +378,12 @@ class ChuniStaticData(BaseData): return result.lastrowid async def get_login_bonus_presets( - self, version: int, is_enabled: bool = True + self, version: int, isEnabled: bool = True ) -> Optional[List[Row]]: sql = login_bonus_preset.select( and_( 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 result.fetchone() - async def put_avatar( self, version: int, @@ -926,4 +957,86 @@ class ChuniStaticData(BaseData): result = await self.execute(sql) if result is None: return None - return result.fetchone() \ No newline at end of file + 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 diff --git a/titles/cm/read.py b/titles/cm/read.py index b4b3b5e..d0db43c 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -325,3 +325,39 @@ class CardMakerReader(BaseReader): maxSelectPoint=max_select_point, ) 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) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 0d13a0d..68d3e80 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -1,3 +1,6 @@ +from typing import Optional +from core.utils import floor_to_nearest_005 + class Mai2Constants: GRADE = { "D": 0, @@ -83,6 +86,46 @@ class Mai2Constants: "maimai DX BUDDiES PLUS" ) + 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, + } + @classmethod 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] + + @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) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index ddba0f8..33b93c6 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -2,13 +2,27 @@ from core.data.schema.base import BaseData, metadata from typing import Optional, Dict, List 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.sql import func, select from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert 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( "mai2_static_event", metadata, @@ -19,6 +33,7 @@ event = Table( Column("name", String(255)), Column("startDate", TIMESTAMP, server_default=func.now()), 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"), mysql_charset="utf8mb4", ) @@ -37,6 +52,7 @@ music = Table( Column("addedVersion", String(255)), Column("difficulty", Float), 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"), mysql_charset="utf8mb4", ) @@ -51,6 +67,7 @@ ticket = Table( Column("name", String(255)), Column("price", Integer, 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"), mysql_charset="utf8mb4", ) @@ -67,6 +84,7 @@ cards = Table( 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("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"), mysql_charset="utf8mb4", ) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 71ea7f2..f218fcc 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -1,6 +1,6 @@ -from typing import Final, Dict +from typing import Optional from enum import Enum - +from core.utils import floor_to_nearest_005 class OngekiConstants: GAME_CODE = "SDDT" @@ -106,6 +106,24 @@ class OngekiConstants: "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 def game_ver_to_string(cls, ver: int): 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) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 5d6a3e0..8609f5c 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional 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.sql import func, select 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.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( "ongeki_static_events", metadata, @@ -20,6 +51,7 @@ events = Table( Column("startDate", TIMESTAMP, server_default=func.now()), Column("endDate", TIMESTAMP, server_default=func.now()), 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"), mysql_charset="utf8mb4", ) @@ -36,6 +68,7 @@ music = Table( Column("artist", String(255)), Column("genre", String(255)), Column("level", Float), + Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"), mysql_charset="utf8mb4", ) @@ -59,6 +92,7 @@ gachas = Table( 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("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"), mysql_charset="utf8mb4", ) @@ -94,6 +128,7 @@ cards = Table( Column("skillId", Integer, nullable=False), Column("choKaikaSkillId", Integer, nullable=False), Column("cardNumber", String(255)), + Column("opt", ForeignKey("ongeki_static_opt.id", ondelete="SET NULL", onupdate="cascade")), UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), mysql_charset="utf8mb4", ) @@ -107,6 +142,7 @@ rewards = Table( Column("rewardname", String(255), nullable=False), Column("itemKind", 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"), mysql_charset="utf8mb4", )