1
0
Fork 0

db: Migrate to SQLite, improve performance

This commit is contained in:
beerpiss 2023-08-17 17:16:05 +07:00
parent fc947d36a5
commit f07d60f8ce
107 changed files with 15925 additions and 14210 deletions

7
.gitignore vendored
View File

@ -160,4 +160,9 @@ config/*
deliver/*
*.gz
dbdump-*.json
dbdump-*.json
*.sqlite3
*.sqlite3-journal
*.sqlite3-shm
*.sqlite3-wal

View File

@ -80,7 +80,14 @@ class AllnetServlet:
req = AllnetPowerOnRequest(req_dict[0])
# Validate the request. Currently we only validate the fields we plan on using
if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver:
if (
not req.game_id
or not req.ver
or not req.serial
or not req.ip
or not req.firm_ver
or not req.boot_ver
):
raise AllnetRequestException(
f"Bad auth request params from {request_ip} - {vars(req)}"
)
@ -97,7 +104,7 @@ class AllnetServlet:
else:
resp = AllnetPowerOnResponse()
self.logger.debug(f"Allnet request: {vars(req)}")
self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry:
if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
@ -108,7 +115,9 @@ class AllnetServlet:
resp.stat = -1
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
return (
urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n"
).encode("utf-8")
else:
self.logger.info(
@ -116,16 +125,16 @@ class AllnetServlet:
)
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_str}")
return (resp_str + "\n").encode("utf-8")
resp.uri, resp.host = self.uri_registry[req.game_id]
machine = self.data.arcade.get_machine(req.serial)
machine = self.data.arcade.get_machine(req.serial)
if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
self.data.base.log_event(
@ -135,7 +144,9 @@ class AllnetServlet:
resp.stat = -2
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
return (
urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n"
).encode("utf-8")
if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"])
@ -180,7 +191,7 @@ class AllnetServlet:
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_dict}")
self.logger.debug(f"Allnet response: {resp_dict}")
resp_str += "\n"
return resp_str.encode("utf-8")
@ -228,7 +239,12 @@ class AllnetServlet:
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
self.logger.debug(f"Sending download uri {resp.uri}")
self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
self.data.base.log_event(
"allnet",
"DLORDER_REQ_SUCCESS",
logging.INFO,
f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}",
)
return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
@ -239,8 +255,15 @@ class AllnetServlet:
req_file = match["file"].replace("%0A", "")
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
self.logger.info(
f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful"
)
self.data.base.log_event(
"allnet",
"DLORDER_INI_SENT",
logging.INFO,
f"{Utils.get_ip_addr(request)} successfully recieved {req_file}",
)
return open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
).read()
@ -257,7 +280,7 @@ class AllnetServlet:
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
req_data = request.content.getvalue()
sections = req_data.decode("utf-8").split("\r\n")
req_dict = dict(urllib.parse.parse_qsl(sections[0]))
serial: Union[str, None] = req_dict.get("serial", None)
@ -266,12 +289,19 @@ class AllnetServlet:
dl_state: Union[str, None] = req_dict.get("dld_st", None)
ip = Utils.get_ip_addr(request)
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
if (
serial is None
or num_files_dld is None
or num_files_to_dl is None
or dl_state is None
):
return "NG".encode()
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
self.logger.info(
f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})"
)
return "OK".encode()
def handle_alive(self, request: Request, match: Dict) -> bytes:
return "OK".encode()
@ -297,7 +327,7 @@ class AllnetServlet:
kc_game: str = req_dict[0]["gameid"]
kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S")
kc_serial_bytes = kc_serial.encode()
except KeyError as e:
return f"result=5&linelimit=&message={e} field is missing".encode()
@ -431,6 +461,7 @@ class AllnetPowerOnRequest:
self.format_ver = float(req.get("format_ver", "1.00"))
self.token: str = req.get("token", "0")
class AllnetPowerOnResponse:
def __init__(self) -> None:
self.stat = 1
@ -443,7 +474,7 @@ class AllnetPowerOnResponse:
self.region_name0 = "W"
self.region_name1 = ""
self.region_name2 = ""
self.region_name3 = ""
self.region_name3 = ""
self.setting = "1"
self.year = datetime.now().year
self.month = datetime.now().month
@ -452,6 +483,7 @@ class AllnetPowerOnResponse:
self.minute = datetime.now().minute
self.second = datetime.now().second
class AllnetPowerOnResponse3(AllnetPowerOnResponse):
def __init__(self, token) -> None:
super().__init__()

View File

@ -111,7 +111,7 @@ class DatabaseConfig:
@property
def protocol(self) -> str:
return CoreConfig.get_config_field(
self.__config, "core", "database", "type", default="mysql"
self.__config, "core", "database", "protocol", default="mysql"
)
@property

View File

@ -1,2 +1,2 @@
from core.data.database import Data
from core.data.cache import cached
from core.data.database import Data
from core.data.cache import cached

View File

@ -1,67 +1,67 @@
from typing import Any, Callable
from functools import wraps
import hashlib
import pickle
import logging
from core.config import CoreConfig
cfg: CoreConfig = None # type: ignore
# Make memcache optional
try:
import pylibmc # type: ignore
has_mc = True
except ModuleNotFoundError:
has_mc = False
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
def _cached(func: Callable) -> Callable:
if has_mc:
hostname = "127.0.0.1"
if cfg:
hostname = cfg.database.memcached_host
memcache = pylibmc.Client([hostname], binary=True)
memcache.behaviors = {"tcp_nodelay": True, "ketama": True}
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if lifetime is not None:
# Hash function args
items = kwargs.items()
hashable_args = (args[1:], sorted(list(items)))
args_key = hashlib.md5(pickle.dumps(hashable_args)).hexdigest()
# Generate unique cache key
cache_key = f'{func.__module__}-{func.__name__}-{args_key}-{extra_key() if hasattr(extra_key, "__call__") else extra_key}'
# Return cached version if allowed and available
try:
result = memcache.get(cache_key)
except pylibmc.Error as e:
logging.getLogger("database").error(f"Memcache failed: {e}")
result = None
if result is not None:
logging.getLogger("database").debug(f"Cache hit: {result}")
return result
# Generate output
result = func(*args, **kwargs)
# Cache output if allowed
if lifetime is not None and result is not None:
logging.getLogger("database").debug(f"Setting cache: {result}")
memcache.set(cache_key, result, lifetime)
return result
else:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)
return wrapper
return _cached
from typing import Any, Callable
from functools import wraps
import hashlib
import pickle
import logging
from core.config import CoreConfig
cfg: CoreConfig = None # type: ignore
# Make memcache optional
try:
import pylibmc # type: ignore
has_mc = True
except ModuleNotFoundError:
has_mc = False
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
def _cached(func: Callable) -> Callable:
if has_mc:
hostname = "127.0.0.1"
if cfg:
hostname = cfg.database.memcached_host
memcache = pylibmc.Client([hostname], binary=True)
memcache.behaviors = {"tcp_nodelay": True, "ketama": True}
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if lifetime is not None:
# Hash function args
items = kwargs.items()
hashable_args = (args[1:], sorted(list(items)))
args_key = hashlib.md5(pickle.dumps(hashable_args)).hexdigest()
# Generate unique cache key
cache_key = f'{func.__module__}-{func.__name__}-{args_key}-{extra_key() if hasattr(extra_key, "__call__") else extra_key}'
# Return cached version if allowed and available
try:
result = memcache.get(cache_key)
except pylibmc.Error as e:
logging.getLogger("database").error(f"Memcache failed: {e}")
result = None
if result is not None:
logging.getLogger("database").debug(f"Cache hit: {result}")
return result
# Generate output
result = func(*args, **kwargs)
# Cache output if allowed
if lifetime is not None and result is not None:
logging.getLogger("database").debug(f"Setting cache: {result}")
memcache.set(cache_key, result, lifetime)
return result
else:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)
return wrapper
return _cached

View File

@ -1,357 +1,370 @@
import logging, coloredlogs
from typing import Optional, Dict, List
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import create_engine
from logging.handlers import TimedRotatingFileHandler
import importlib, os
import secrets, string
import bcrypt
from hashlib import sha256
from core.config import CoreConfig
from core.data.schema import *
from core.utils import Utils
class Data:
current_schema_version = 4
engine = None
session = None
user = None
arcade = None
card = None
base = None
def __init__(self, cfg: CoreConfig) -> None:
self.config = cfg
if self.config.database.sha2_password:
passwd = sha256(self.config.database.password.encode()).digest()
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
else:
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
if Data.engine is None:
Data.engine = create_engine(self.__url, pool_recycle=3600)
self.__engine = Data.engine
if Data.session is None:
s = sessionmaker(bind=Data.engine, autoflush=True, autocommit=True)
Data.session = scoped_session(s)
if Data.user is None:
Data.user = UserData(self.config, self.session)
if Data.arcade is None:
Data.arcade = ArcadeData(self.config, self.session)
if Data.card is None:
Data.card = CardData(self.config, self.session)
if Data.base is None:
Data.base = BaseData(self.config, self.session)
self.logger = logging.getLogger("database")
# Prevent the logger from adding handlers multiple times
if not getattr(self.logger, "handler_set", None):
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "db"),
encoding="utf-8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.config.database.loglevel)
coloredlogs.install(
cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.handler_set = True # type: ignore
def create_database(self):
self.logger.info("Creating databases...")
try:
metadata.create_all(self.__engine.connect())
except SQLAlchemyError as e:
self.logger.error(f"Failed to create databases! {e}")
return
games = Utils.get_all_titles()
for game_dir, game_mod in games.items():
try:
if hasattr(game_mod, "database") and hasattr(
game_mod, "current_schema_version"
):
game_mod.database(self.config)
metadata.create_all(self.__engine.connect())
self.base.touch_schema_ver(
game_mod.current_schema_version, game_mod.game_codes[0]
)
except Exception as e:
self.logger.warning(
f"Could not load database schema from {game_dir} - {e}"
)
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
self.base.set_schema_ver(self.current_schema_version)
self.logger.info(
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
)
self.user.reset_autoincrement(
self.config.database.user_table_autoincrement_start
)
def recreate_database(self):
self.logger.info("Dropping all databases...")
self.base.execute("SET FOREIGN_KEY_CHECKS=0")
try:
metadata.drop_all(self.__engine.connect())
except SQLAlchemyError as e:
self.logger.error(f"Failed to drop databases! {e}")
return
for root, dirs, files in os.walk("./titles"):
for dir in dirs:
if not dir.startswith("__"):
try:
mod = importlib.import_module(f"titles.{dir}")
try:
if hasattr(mod, "database"):
mod.database(self.config)
metadata.drop_all(self.__engine.connect())
except Exception as e:
self.logger.warning(
f"Could not load database schema from {dir} - {e}"
)
except ImportError as e:
self.logger.warning(
f"Failed to load database schema dir {dir} - {e}"
)
break
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
self.create_database()
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
old_ver = self.base.get_schema_ver(game)
sql = ""
if version is None:
if not game == "CORE":
titles = Utils.get_all_titles()
for folder, mod in titles.items():
if not mod.game_codes[0] == game:
continue
if hasattr(mod, "current_schema_version"):
version = mod.current_schema_version
else:
self.logger.warn(
f"current_schema_version not found for {folder}"
)
else:
version = self.current_schema_version
if version is None:
self.logger.warn(
f"Could not determine latest version for {game}, please specify --version"
)
if old_ver is None:
self.logger.error(
f"Schema for game {game} does not exist, did you run the creation script?"
)
return
if old_ver == version:
self.logger.info(
f"Schema for game {game} is already version {old_ver}, nothing to do"
)
return
if action == "upgrade":
for x in range(old_ver, version):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
else:
for x in range(old_ver, version, -1):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
result = self.base.set_schema_ver(version, game)
if result is None:
self.logger.error("Error setting version in schema_version table!")
return None
self.logger.info(f"Successfully migrated {game} to schema version {version}")
def create_owner(self, email: Optional[str] = None) -> None:
pw = "".join(
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
)
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
user_id = self.user.create_user(email=email, permission=255, password=hash)
if user_id is None:
self.logger.error(f"Failed to create owner with email {email}")
return
card_id = self.card.create_card(user_id, "00000000000000000000")
if card_id is None:
self.logger.error(f"Failed to create card for owner with id {user_id}")
return
self.logger.warn(
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
)
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
if old_ac == new_ac:
self.logger.error("Both access codes are the same!")
return
new_card = self.card.get_card_by_access_code(new_ac)
if new_card is None:
self.card.update_access_code(old_ac, new_ac)
return
if not should_force:
self.logger.warn(
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
return
self.logger.info(
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
self.card.delete_card(new_card["id"])
self.card.update_access_code(old_ac, new_ac)
hanging_user = self.user.get_user(new_card["user"])
if hanging_user["password"] is None:
self.logger.info(f"Delete hanging user {hanging_user['id']}")
self.user.delete_user(hanging_user["id"])
def delete_hanging_users(self) -> None:
"""
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
"""
unreg_users = self.user.get_unregistered_users()
if unreg_users is None:
self.logger.error("Error occoured finding unregistered users")
for user in unreg_users:
cards = self.card.get_user_cards(user["id"])
if cards is None:
self.logger.error(f"Error getting cards for user {user['id']}")
continue
if not cards:
self.logger.info(f"Delete hanging user {user['id']}")
self.user.delete_user(user["id"])
def autoupgrade(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
if all_game_versions is None:
self.logger.warn("Failed to get schema versions")
return
all_games = Utils.get_all_titles()
all_games_list: Dict[str, int] = {}
for _, mod in all_games.items():
if hasattr(mod, "current_schema_version"):
all_games_list[mod.game_codes[0]] = mod.current_schema_version
for x in all_game_versions:
failed = False
game = x["game"].upper()
update_ver = int(x["version"])
latest_ver = all_games_list.get(game, 1)
if game == "CORE":
latest_ver = self.current_schema_version
if update_ver == latest_ver:
self.logger.info(f"{game} is already latest version")
continue
for y in range(update_ver + 1, latest_ver + 1):
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
with open(
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error(
f"Error execuing sql script for game {game} v{y}!"
)
failed = True
break
else:
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
failed = True
if not failed:
self.base.set_schema_ver(latest_ver, game)
def show_versions(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
for ver in all_game_versions:
self.logger.info(f"{ver['game']} -> v{ver['version']}")
import logging, coloredlogs
from typing import Optional, Dict, List
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import create_engine
from logging.handlers import TimedRotatingFileHandler
import importlib, os
import secrets, string
import bcrypt
from hashlib import sha256
from core.config import CoreConfig
from core.data.schema import *
from core.utils import Utils
class Data:
current_schema_version = 4
engine = None
session = None
user = None
arcade = None
card = None
base = None
def __init__(self, cfg: CoreConfig) -> None:
self.config = cfg
if self.config.database.protocol == "sqlite":
self.__url = (
f"{self.config.database.protocol}:///{self.config.database.name}"
)
else:
if self.config.database.sha2_password:
passwd = sha256(self.config.database.password.encode()).digest()
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
else:
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
if Data.engine is None:
Data.engine = create_engine(self.__url, pool_recycle=3600)
self.__engine = Data.engine
if Data.session is None:
s = sessionmaker(
bind=Data.engine,
# There are a billion transactions, autoflushing will take a lot of time
# and potentially locks up the database.
autoflush=self.config.database.protocol != "sqlite",
autocommit=True,
)
Data.session = scoped_session(s)
if Data.user is None:
Data.user = UserData(self.config, self.session)
if Data.arcade is None:
Data.arcade = ArcadeData(self.config, self.session)
if Data.card is None:
Data.card = CardData(self.config, self.session)
if Data.base is None:
Data.base = BaseData(self.config, self.session)
self.logger = logging.getLogger("database")
# Prevent the logger from adding handlers multiple times
if not getattr(self.logger, "handler_set", None):
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "db"),
encoding="utf-8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.config.database.loglevel)
coloredlogs.install(
cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.handler_set = True # type: ignore
def create_database(self):
self.logger.info("Creating databases...")
self.base.setup_sqlite()
try:
metadata.create_all(self.__engine.connect())
except SQLAlchemyError as e:
self.logger.error(f"Failed to create databases! {e}")
return
games = Utils.get_all_titles()
for game_dir, game_mod in games.items():
try:
if hasattr(game_mod, "database") and hasattr(
game_mod, "current_schema_version"
):
game_mod.database(self.config)
metadata.create_all(self.__engine.connect())
self.base.touch_schema_ver(
game_mod.current_schema_version, game_mod.game_codes[0]
)
except Exception as e:
self.logger.warning(
f"Could not load database schema from {game_dir} - {e}"
)
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
self.base.set_schema_ver(self.current_schema_version)
self.logger.info(
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
)
self.user.reset_autoincrement(
self.config.database.user_table_autoincrement_start
)
def recreate_database(self):
self.logger.info("Dropping all databases...")
self.base.execute("SET FOREIGN_KEY_CHECKS=0")
try:
metadata.drop_all(self.__engine.connect())
except SQLAlchemyError as e:
self.logger.error(f"Failed to drop databases! {e}")
return
for root, dirs, files in os.walk("./titles"):
for dir in dirs:
if not dir.startswith("__"):
try:
mod = importlib.import_module(f"titles.{dir}")
try:
if hasattr(mod, "database"):
mod.database(self.config)
metadata.drop_all(self.__engine.connect())
except Exception as e:
self.logger.warning(
f"Could not load database schema from {dir} - {e}"
)
except ImportError as e:
self.logger.warning(
f"Failed to load database schema dir {dir} - {e}"
)
break
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
self.create_database()
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
old_ver = self.base.get_schema_ver(game)
sql = ""
if version is None:
if not game == "CORE":
titles = Utils.get_all_titles()
for folder, mod in titles.items():
if not mod.game_codes[0] == game:
continue
if hasattr(mod, "current_schema_version"):
version = mod.current_schema_version
else:
self.logger.warn(
f"current_schema_version not found for {folder}"
)
else:
version = self.current_schema_version
if version is None:
self.logger.warn(
f"Could not determine latest version for {game}, please specify --version"
)
if old_ver is None:
self.logger.error(
f"Schema for game {game} does not exist, did you run the creation script?"
)
return
if old_ver == version:
self.logger.info(
f"Schema for game {game} is already version {old_ver}, nothing to do"
)
return
if action == "upgrade":
for x in range(old_ver, version):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
else:
for x in range(old_ver, version, -1):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
result = self.base.set_schema_ver(version, game)
if result is None:
self.logger.error("Error setting version in schema_version table!")
return None
self.logger.info(f"Successfully migrated {game} to schema version {version}")
def create_owner(self, email: Optional[str] = None) -> None:
pw = "".join(
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
)
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
user_id = self.user.create_user(email=email, permission=255, password=hash)
if user_id is None:
self.logger.error(f"Failed to create owner with email {email}")
return
card_id = self.card.create_card(user_id, "00000000000000000000")
if card_id is None:
self.logger.error(f"Failed to create card for owner with id {user_id}")
return
self.logger.warn(
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
)
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
if old_ac == new_ac:
self.logger.error("Both access codes are the same!")
return
new_card = self.card.get_card_by_access_code(new_ac)
if new_card is None:
self.card.update_access_code(old_ac, new_ac)
return
if not should_force:
self.logger.warn(
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
return
self.logger.info(
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
self.card.delete_card(new_card["id"])
self.card.update_access_code(old_ac, new_ac)
hanging_user = self.user.get_user(new_card["user"])
if hanging_user["password"] is None:
self.logger.info(f"Delete hanging user {hanging_user['id']}")
self.user.delete_user(hanging_user["id"])
def delete_hanging_users(self) -> None:
"""
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
"""
unreg_users = self.user.get_unregistered_users()
if unreg_users is None:
self.logger.error("Error occoured finding unregistered users")
for user in unreg_users:
cards = self.card.get_user_cards(user["id"])
if cards is None:
self.logger.error(f"Error getting cards for user {user['id']}")
continue
if not cards:
self.logger.info(f"Delete hanging user {user['id']}")
self.user.delete_user(user["id"])
def autoupgrade(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
if all_game_versions is None:
self.logger.warn("Failed to get schema versions")
return
all_games = Utils.get_all_titles()
all_games_list: Dict[str, int] = {}
for _, mod in all_games.items():
if hasattr(mod, "current_schema_version"):
all_games_list[mod.game_codes[0]] = mod.current_schema_version
for x in all_game_versions:
failed = False
game = x["game"].upper()
update_ver = int(x["version"])
latest_ver = all_games_list.get(game, 1)
if game == "CORE":
latest_ver = self.current_schema_version
if update_ver == latest_ver:
self.logger.info(f"{game} is already latest version")
continue
for y in range(update_ver + 1, latest_ver + 1):
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
with open(
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error(
f"Error execuing sql script for game {game} v{y}!"
)
failed = True
break
else:
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
failed = True
if not failed:
self.base.set_schema_ver(latest_ver, game)
def show_versions(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
for ver in all_game_versions:
self.logger.info(f"{ver['game']} -> v{ver['version']}")

View File

@ -1,6 +1,6 @@
from core.data.schema.user import UserData
from core.data.schema.card import CardData
from core.data.schema.base import BaseData, metadata
from core.data.schema.arcade import ArcadeData
__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"]
from core.data.schema.user import UserData
from core.data.schema.card import CardData
from core.data.schema.base import BaseData, metadata
from core.data.schema.arcade import ArcadeData
__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"]

View File

@ -1,219 +1,219 @@
from typing import Optional, Dict
from sqlalchemy import Table, Column
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import Integer, String, Boolean
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
import re
from core.data.schema.base import BaseData, metadata
from core.const import *
arcade = Table(
"arcade",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("name", String(255)),
Column("nickname", String(255)),
Column("country", String(3)),
Column("country_id", Integer),
Column("state", String(255)),
Column("city", String(255)),
Column("region_id", Integer),
Column("timezone", String(255)),
mysql_charset="utf8mb4",
)
machine = Table(
"machine",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"arcade",
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("serial", String(15), nullable=False),
Column("board", String(15)),
Column("game", String(4)),
Column("country", String(3)), # overwrites if not null
Column("timezone", String(255)),
Column("ota_enable", Boolean),
Column("is_cab", Boolean),
mysql_charset="utf8mb4",
)
arcade_owner = Table(
"arcade_owner",
metadata,
Column(
"user",
Integer,
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column(
"arcade",
Integer,
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("permissions", Integer, nullable=False),
PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"),
mysql_charset="utf8mb4",
)
class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
if serial is not None:
serial = serial.replace("-", "")
if len(serial) == 11:
sql = machine.select(machine.c.serial.like(f"{serial}%"))
elif len(serial) == 15:
sql = machine.select(machine.c.serial == serial)
else:
self.logger.error(f"{__name__ }: Malformed serial {serial}")
return None
elif id is not None:
sql = machine.select(machine.c.id == id)
else:
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
return None
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_machine(
self,
arcade_id: int,
serial: str = "",
board: str = None,
game: str = None,
is_cab: bool = False,
) -> Optional[int]:
if arcade_id:
self.logger.error(f"{__name__ }: Need arcade id!")
return None
sql = machine.insert().values(
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(keychip=serial)
)
if result is None:
self.logger.error(
f"Failed to update serial for machine {machine_id} -> {serial}"
)
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(board=boardid)
)
if result is None:
self.logger.error(
f"Failed to update board id for machine {machine_id} -> {boardid}"
)
def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_arcade(
self,
name: str,
nickname: str = None,
country: str = "JPN",
country_id: int = 1,
state: str = "",
city: str = "",
regional_id: int = 1,
) -> Optional[int]:
if nickname is None:
nickname = name
sql = arcade.insert().values(
name=name,
nickname=nickname,
country=country,
country_id=country_id,
state=state,
city=city,
regional_id=regional_id,
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def format_serial(
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
)
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
num = serial[7:11]
append = serial[11:15]
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
self.logger.error(f"Serial validate failed: {serial} failed regex")
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed append {append}"
)
return False
if len(num) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed number {num}"
)
return False
return True
from typing import Optional, Dict
from sqlalchemy import Table, Column
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import Integer, String, Boolean
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
import re
from core.data.schema.base import BaseData, metadata
from core.const import *
arcade = Table(
"arcade",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("name", String(255)),
Column("nickname", String(255)),
Column("country", String(3)),
Column("country_id", Integer),
Column("state", String(255)),
Column("city", String(255)),
Column("region_id", Integer),
Column("timezone", String(255)),
mysql_charset="utf8mb4",
)
machine = Table(
"machine",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"arcade",
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("serial", String(15), nullable=False),
Column("board", String(15)),
Column("game", String(4)),
Column("country", String(3)), # overwrites if not null
Column("timezone", String(255)),
Column("ota_enable", Boolean),
Column("is_cab", Boolean),
mysql_charset="utf8mb4",
)
arcade_owner = Table(
"arcade_owner",
metadata,
Column(
"user",
Integer,
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column(
"arcade",
Integer,
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("permissions", Integer, nullable=False),
PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"),
mysql_charset="utf8mb4",
)
class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
if serial is not None:
serial = serial.replace("-", "")
if len(serial) == 11:
sql = machine.select(machine.c.serial.like(f"{serial}%"))
elif len(serial) == 15:
sql = machine.select(machine.c.serial == serial)
else:
self.logger.error(f"{__name__ }: Malformed serial {serial}")
return None
elif id is not None:
sql = machine.select(machine.c.id == id)
else:
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
return None
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_machine(
self,
arcade_id: int,
serial: str = "",
board: str = None,
game: str = None,
is_cab: bool = False,
) -> Optional[int]:
if arcade_id:
self.logger.error(f"{__name__ }: Need arcade id!")
return None
sql = machine.insert().values(
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(keychip=serial)
)
if result is None:
self.logger.error(
f"Failed to update serial for machine {machine_id} -> {serial}"
)
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(board=boardid)
)
if result is None:
self.logger.error(
f"Failed to update board id for machine {machine_id} -> {boardid}"
)
def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_arcade(
self,
name: str,
nickname: str = None,
country: str = "JPN",
country_id: int = 1,
state: str = "",
city: str = "",
regional_id: int = 1,
) -> Optional[int]:
if nickname is None:
nickname = name
sql = arcade.insert().values(
name=name,
nickname=nickname,
country=country,
country_id=country_id,
state=state,
city=city,
regional_id=regional_id,
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def format_serial(
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
)
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
num = serial[7:11]
append = serial[11:15]
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
self.logger.error(f"Serial validate failed: {serial} failed regex")
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed append {append}"
)
return False
if len(num) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed number {num}"
)
return False
return True

View File

@ -1,165 +1,170 @@
import json
import logging
from random import randrange
from typing import Any, Optional, Dict, List
from sqlalchemy.engine import Row
from sqlalchemy.engine.cursor import CursorResult
from sqlalchemy.engine.base import Connection
from sqlalchemy.sql import text, func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import MetaData, Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON
from sqlalchemy.dialects.mysql import insert
from core.config import CoreConfig
metadata = MetaData()
schema_ver = Table(
"schema_versions",
metadata,
Column("game", String(4), primary_key=True, nullable=False),
Column("version", Integer, nullable=False, server_default="1"),
mysql_charset="utf8mb4",
)
event_log = Table(
"event_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("system", String(255), nullable=False),
Column("type", String(255), nullable=False),
Column("severity", Integer, nullable=False),
Column("message", String(1000), nullable=False),
Column("details", JSON, nullable=False),
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
mysql_charset="utf8mb4",
)
class BaseData:
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
self.config = cfg
self.conn = conn
self.logger = logging.getLogger("database")
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
res = None
try:
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
res = self.conn.execute(text(sql), opts)
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
except Exception:
try:
res = self.conn.execute(sql, opts)
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
except Exception:
self.logger.error(f"Unknown error")
raise
return res
def generate_id(self) -> int:
"""
Generate a random 5-7 digit id
"""
return randrange(10000, 9999999)
def get_all_schema_vers(self) -> Optional[List[Row]]:
sql = select(schema_ver)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_schema_ver(self, game: str) -> Optional[int]:
sql = select(schema_ver).where(schema_ver.c.game == game)
result = self.execute(sql)
if result is None:
return None
row = result.fetchone()
if row is None:
return None
return row["version"]
def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_duplicate_key_update(version=schema_ver.c.version)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_duplicate_key_update(version=ver)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def log_event(
self, system: str, type: str, severity: int, message: str, details: Dict = {}
) -> Optional[int]:
sql = event_log.insert().values(
system=system,
type=type,
severity=severity,
message=message,
details=json.dumps(details),
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}"
)
return None
return result.lastrowid
def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
sql = event_log.select().limit(entries).all()
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def fix_bools(self, data: Dict) -> Dict:
for k, v in data.items():
if type(v) == str and v.lower() == "true":
data[k] = True
elif type(v) == str and v.lower() == "false":
data[k] = False
return data
import json
import logging
from random import randrange
from typing import Any, Optional, Dict, List
from sqlalchemy.engine import Row
from sqlalchemy.engine.cursor import CursorResult
from sqlalchemy.engine.base import Connection
from sqlalchemy.sql import text, func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import MetaData, Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON
from sqlalchemy.dialects.sqlite import insert
from core.config import CoreConfig
metadata = MetaData()
schema_ver = Table(
"schema_versions",
metadata,
Column("game", String(4), primary_key=True, nullable=False),
Column("version", Integer, nullable=False, server_default="1"),
mysql_charset="utf8mb4",
)
event_log = Table(
"event_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("system", String(255), nullable=False),
Column("type", String(255), nullable=False),
Column("severity", Integer, nullable=False),
Column("message", String(1000), nullable=False),
Column("details", JSON, nullable=False),
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
mysql_charset="utf8mb4",
)
class BaseData:
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
self.config = cfg
self.conn = conn
self.logger = logging.getLogger("database")
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
res = None
try:
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
res = self.conn.execute(text(sql), opts)
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
except Exception:
try:
res = self.conn.execute(sql, opts)
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
except Exception:
self.logger.error(f"Unknown error")
raise
return res
def setup_sqlite(self) -> None:
if self.config.database.protocol == "sqlite":
self.execute("PRAGMA journal_mode=WAL")
self.execute("PRAGMA synchronous=NORMAL")
def generate_id(self) -> int:
"""
Generate a random 5-7 digit id
"""
return randrange(10000, 9999999)
def get_all_schema_vers(self) -> Optional[List[Row]]:
sql = select(schema_ver)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_schema_ver(self, game: str) -> Optional[int]:
sql = select(schema_ver).where(schema_ver.c.game == game)
result = self.execute(sql)
if result is None:
return None
row = result.fetchone()
if row is None:
return None
return row["version"]
def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_conflict_do_update(set_=dict(version=schema_ver.c.version))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_conflict_do_update(set_=dict(version=ver))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def log_event(
self, system: str, type: str, severity: int, message: str, details: Dict = {}
) -> Optional[int]:
sql = event_log.insert().values(
system=system,
type=type,
severity=severity,
message=message,
details=json.dumps(details),
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}"
)
return None
return result.lastrowid
def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
sql = event_log.select().limit(entries).all()
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def fix_bools(self, data: Dict) -> Dict:
for k, v in data.items():
if type(v) == str and v.lower() == "true":
data[k] = True
elif type(v) == str and v.lower() == "false":
data[k] = False
return data

View File

@ -1,104 +1,104 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.engine import Row
from core.data.schema.base import BaseData, metadata
aime_card = Table(
"aime_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("access_code", String(20)),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
Column("is_banned", Boolean, server_default="0"),
UniqueConstraint("user", "access_code", name="aime_card_uk"),
mysql_charset="utf8mb4",
)
class CardData(BaseData):
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
sql = aime_card.select(aime_card.c.access_code == access_code)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_id(self, card_id: int) -> Optional[Row]:
sql = aime_card.select(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def update_access_code(self, old_ac: str, new_ac: str) -> None:
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
access_code=new_ac
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to change card access code from {old_ac} to {new_ac}"
)
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
"""
Given a 20 digit access code as a string, get the user id associated with that card
"""
card = self.get_card_by_access_code(access_code)
if card is None:
return None
return int(card["user"])
def delete_card(self, card_id: int) -> None:
sql = aime_card.delete(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to delete card with id {card_id}")
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
"""
Returns all cards owned by a user
"""
sql = aime_card.select(aime_card.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
"""
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
"""
sql = aime_card.insert().values(user=user_id, access_code=access_code)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
"""
return f"{int(luid, base=16):0{20}}"
def to_idm(self, access_code: str) -> str:
"""
Given a 20 digit access code as a string, return the 16 hex character luid
"""
return f"{int(access_code):0{16}x}"
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.engine import Row
from core.data.schema.base import BaseData, metadata
aime_card = Table(
"aime_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("access_code", String(20)),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
Column("is_banned", Boolean, server_default="0"),
UniqueConstraint("user", "access_code", name="aime_card_uk"),
mysql_charset="utf8mb4",
)
class CardData(BaseData):
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
sql = aime_card.select(aime_card.c.access_code == access_code)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_id(self, card_id: int) -> Optional[Row]:
sql = aime_card.select(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def update_access_code(self, old_ac: str, new_ac: str) -> None:
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
access_code=new_ac
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to change card access code from {old_ac} to {new_ac}"
)
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
"""
Given a 20 digit access code as a string, get the user id associated with that card
"""
card = self.get_card_by_access_code(access_code)
if card is None:
return None
return int(card["user"])
def delete_card(self, card_id: int) -> None:
sql = aime_card.delete(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to delete card with id {card_id}")
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
"""
Returns all cards owned by a user
"""
sql = aime_card.select(aime_card.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
"""
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
"""
sql = aime_card.insert().values(user=user_id, access_code=access_code)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
"""
return f"{int(luid, base=16):0{20}}"
def to_idm(self, access_code: str) -> str:
"""
Given a 20 digit access code as a string, return the 16 hex character luid
"""
return f"{int(access_code):0{16}x}"

View File

@ -1,109 +1,116 @@
from enum import Enum
from typing import Optional, List
from sqlalchemy import Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
import bcrypt
from core.data.schema.base import BaseData, metadata
aime_user = Table(
"aime_user",
metadata,
Column("id", Integer, nullable=False, primary_key=True, autoincrement=True),
Column("username", String(25), unique=True),
Column("email", String(255), unique=True),
Column("password", String(255)),
Column("permissions", Integer),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("suspend_expire_time", TIMESTAMP),
mysql_charset="utf8mb4",
)
class PermissionBits(Enum):
PermUser = 1
PermMod = 2
PermSysAdmin = 4
class UserData(BaseData):
def create_user(
self,
id: int = None,
username: str = None,
email: str = None,
password: str = None,
permission: int = 1,
) -> Optional[int]:
if id is None:
sql = insert(aime_user).values(
username=username,
email=email,
password=password,
permissions=permission,
)
else:
sql = insert(aime_user).values(
id=id,
username=username,
email=email,
password=password,
permissions=permission,
)
conflict = sql.on_duplicate_key_update(
username=username, email=email, password=password, permissions=permission
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_user(self, user_id: int) -> Optional[Row]:
sql = select(aime_user).where(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
usr = self.get_user(user_id)
if usr is None:
return False
if usr["password"] is None:
return False
if passwd is None or not passwd:
return False
return bcrypt.checkpw(passwd, usr["password"].encode())
def reset_autoincrement(self, ai_value: int) -> None:
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
self.execute(sql)
def delete_user(self, user_id: int) -> None:
sql = aime_user.delete(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to delete user with id {user_id}")
def get_unregistered_users(self) -> List[Row]:
"""
Returns a list of users who have not registered with the webui. They may or may not have cards.
"""
sql = select(aime_user).where(aime_user.c.password == None)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
from enum import Enum
from typing import Optional, List
from sqlalchemy import Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP
from sqlalchemy.sql import func
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
import bcrypt
from core.data.schema.base import BaseData, metadata
aime_user = Table(
"aime_user",
metadata,
Column("id", Integer, nullable=False, primary_key=True, autoincrement=True),
Column("username", String(25), unique=True),
Column("email", String(255), unique=True),
Column("password", String(255)),
Column("permissions", Integer),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("suspend_expire_time", TIMESTAMP),
mysql_charset="utf8mb4",
sqlite_autoincrement=True,
)
class PermissionBits(Enum):
PermUser = 1
PermMod = 2
PermSysAdmin = 4
class UserData(BaseData):
def create_user(
self,
id: int = None,
username: str = None,
email: str = None,
password: str = None,
permission: int = 1,
) -> Optional[int]:
if id is None:
sql = insert(aime_user).values(
username=username,
email=email,
password=password,
permissions=permission,
)
else:
sql = insert(aime_user).values(
id=id,
username=username,
email=email,
password=password,
permissions=permission,
)
conflict = sql.on_conflict_do_update(
set_=dict(
username=username,
email=email,
password=password,
permissions=permission,
)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_user(self, user_id: int) -> Optional[Row]:
sql = select(aime_user).where(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
usr = self.get_user(user_id)
if usr is None:
return False
if usr["password"] is None:
return False
if passwd is None or not passwd:
return False
return bcrypt.checkpw(passwd, usr["password"].encode())
def reset_autoincrement(self, ai_value: int) -> None:
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
# sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
sql = f"INSERT OR REPLACE INTO sqlite_sequence VALUES ('aime_user', {ai_value})"
self.execute(sql)
def delete_user(self, user_id: int) -> None:
sql = aime_user.delete(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to delete user with id {user_id}")
def get_unregistered_users(self) -> List[Row]:
"""
Returns a list of users who have not registered with the webui. They may or may not have cards.
"""
sql = select(aime_user).where(aime_user.c.password == None)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -227,22 +227,25 @@ class FE_User(FE_Base):
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request)
cards = self.data.card.get_user_cards(usr_sesh.userId)
user = self.data.user.get_user(usr_sesh.userId)
card_data = []
for c in cards:
if c['is_locked']:
status = 'Locked'
elif c['is_banned']:
status = 'Banned'
if c["is_locked"]:
status = "Locked"
elif c["is_banned"]:
status = "Banned"
else:
status = 'Active'
card_data.append({'access_code': c['access_code'], 'status': status})
status = "Active"
card_data.append({"access_code": c["access_code"], "status": status})
return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username']
title=f"{self.core_config.server.name} | Account",
sesh=vars(usr_sesh),
cards=card_data,
username=user["username"],
).encode("utf-16")

View File

@ -1,6 +1,6 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>Management for {{ username }}</h1>
<h1>Management for {{ username.decode("utf-8") }}</h1>
<h2>Cards <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#card_add">Add</button></h2>
<ul>
{% for c in cards %}
@ -18,7 +18,7 @@
HOW TO:<br>
Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter the 20 digit code below.<br>
!!FOR AMUSEIC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!!
<p /><label for="card_add_frm_access_code">Access Code:&nbsp;</label><input id="card_add_frm_access_code" maxlength="20" type="text" required>
<p /><label for="card_add_frm_access_code">Access Code:&nbsp;</label><input id="card_add_frm_access_code" maxlength="20" type="text" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary">Add</button>
@ -28,4 +28,4 @@
</div>
</div>
{% endblock content %}
{% endblock content %}

View File

@ -35,7 +35,9 @@ class MuchaServlet:
self.logger.addHandler(consoleHandler)
self.logger.setLevel(cfg.mucha.loglevel)
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str
)
all_titles = Utils.get_all_titles()
@ -60,7 +62,7 @@ class MuchaServlet:
return b"RESULTS=000"
req = MuchaAuthRequest(req_dict)
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry:
@ -95,14 +97,17 @@ class MuchaServlet:
return b"RESULTS=000"
req = MuchaUpdateRequest(req_dict)
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
return b"RESULTS=000"
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}")
resp = MuchaUpdateResponse(
req.gameVer,
f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}",
)
self.logger.debug(f"Mucha response {vars(resp)}")
@ -117,9 +122,11 @@ class MuchaServlet:
f"Error processing mucha request {request.content.getvalue()}"
)
return b""
req = MuchaDownloadStateRequest(req_dict)
self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
self.logger.info(
f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}"
)
self.logger.debug(f"request {vars(req)}")
return b"RESULTS=001"
@ -225,7 +232,7 @@ class MuchaUpdateRequest:
class MuchaUpdateResponse:
def __init__(self, game_ver: str, mucha_url: str) -> None:
self.RESULTS = "001"
self.RESULTS = "001"
self.EXE_VER = game_ver
self.UPDATE_VER_1 = game_ver
@ -244,6 +251,7 @@ class MuchaUpdateResponse:
self.USER_ID = ""
self.PASSWORD = ""
"""
RESULTS
EXE_VER
@ -264,13 +272,16 @@ LAN_INFO_SIZE_1
USER_ID
PASSWORD
"""
class MuchaUpdateResponseStub:
def __init__(self, game_ver: str) -> None:
self.RESULTS = "001"
self.UPDATE_VER_1 = game_ver
class MuchaDownloadStateRequest:
def __init__(self, request: Dict) -> None:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.updateVer = request.get("updateVer", "")
self.serialNum = request.get("serialNum", "")

View File

@ -84,7 +84,9 @@ class TitleServlet:
request.setResponseCode(405)
return b""
return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
return index.render_GET(
request, int(endpoints["version"]), endpoints["endpoint"]
)
def render_POST(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]

View File

@ -84,7 +84,7 @@ if __name__ == "__main__":
elif args.action == "cleanup":
data.delete_hanging_users()
elif args.action == "version":
data.show_versions()

View File

@ -12,13 +12,18 @@ title:
hostname: "localhost"
port: 8080
# Only `name` and `protocol` are applicable
# if using SQLite.
database:
host: "localhost"
username: "aime"
password: "aime"
name: "aime"
# Path to database file, if SQLite
name: "database.sqlite3"
port: 3306
protocol: "mysql"
protocol: "sqlite"
sha2_password: False
loglevel: "warn"
user_table_autoincrement_start: 10000

View File

@ -13,6 +13,7 @@ from twisted.web.http import Request
from routes import Mapper
from threading import Thread
class HttpDispatcher(resource.Resource):
def __init__(self, cfg: CoreConfig, config_dir: str):
super().__init__()
@ -188,16 +189,22 @@ class HttpDispatcher(resource.Resource):
if type(ret) == str:
return ret.encode()
elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses
elif (
type(ret) == bytes or type(ret) == tuple
): # allow for bytes or tuple (data, response code) responses
return ret
elif ret is None:
self.logger.warn(f"None returned by controller for {request.uri.decode()} endpoint")
self.logger.warn(
f"None returned by controller for {request.uri.decode()} endpoint"
)
return b""
else:
self.logger.warn(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
self.logger.warn(
f"Unknown data type returned by controller for {request.uri.decode()} endpoint"
)
return b""

View File

@ -1,6 +1,8 @@
# ARTEMiS
A network service emulator for games running SEGA'S ALL.NET service, and similar.
This fork uses SQLite instead of MySQL, intended for local use.
# Supported games
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
@ -36,8 +38,6 @@ Games listed below have been tested and confirmed working. Only game versions ol
## Requirements
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
- pip
- memcached (for non-windows platforms)
- mysql/mariadb server
## Setup guides
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.

View File

@ -2,9 +2,11 @@ from titles.chuni.index import ChuniServlet
from titles.chuni.const import ChuniConstants
from titles.chuni.database import ChuniData
from titles.chuni.read import ChuniReader
from titles.chuni.frontend import ChuniFrontend
index = ChuniServlet
database = ChuniData
reader = ChuniReader
frontend = ChuniFrontend
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
current_schema_version = 4

View File

@ -1,5 +1,6 @@
import logging
import json
from contextlib import nullcontext
from datetime import datetime, timedelta
from time import strftime
@ -40,82 +41,83 @@ class ChuniBase:
user_id = data["userId"]
login_bonus_presets = self.data.static.get_login_bonus_presets(self.version)
for preset in login_bonus_presets:
# check if a user already has some pogress and if not add the
# login bonus entry
user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["presetId"]
)
if user_login_bonus is None:
self.data.item.put_login_bonus(
user_id, self.version, preset["presetId"]
)
# yeah i'm lazy
with self.data.item.conn.begin() or nullcontext():
for preset in login_bonus_presets:
# check if a user already has some pogress and if not add the
# login bonus entry
user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["presetId"]
)
# skip the login bonus entirely if its already finished
if user_login_bonus["isFinished"]:
continue
# make sure the last login is more than 24 hours ago
if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta(
hours=24
):
# increase the login day counter and update the last login date
bonus_count = user_login_bonus["bonusCount"] + 1
last_update_date = datetime.now()
all_login_boni = self.data.static.get_login_bonus(
self.version, preset["presetId"]
)
# skip the current bonus preset if no boni were found
if all_login_boni is None or len(all_login_boni) < 1:
self.logger.warn(
f"No bonus entries found for bonus preset {preset['presetId']}"
if user_login_bonus is None:
self.data.item.put_login_bonus(
user_id, self.version, preset["presetId"]
)
# yeah i'm lazy
user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["presetId"]
)
# skip the login bonus entirely if its already finished
if user_login_bonus["isFinished"]:
continue
max_needed_days = all_login_boni[0]["needLoginDayCount"]
# make sure the last login is more than 24 hours ago
if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta(
hours=24
):
# increase the login day counter and update the last login date
bonus_count = user_login_bonus["bonusCount"] + 1
last_update_date = datetime.now()
# make sure to not show login boni after all days got redeemed
is_finished = False
if bonus_count > max_needed_days:
# assume that all login preset ids under 3000 needs to be
# looped, like 30 and 40 are looped, 40 does not work?
if preset["presetId"] < 3000:
bonus_count = 1
else:
is_finished = True
# grab the item for the corresponding day
login_item = self.data.static.get_login_bonus_by_required_days(
self.version, preset["presetId"], bonus_count
)
if login_item is not None:
# now add the present to the database so the
# handle_get_user_item_api_request can grab them
self.data.item.put_item(
user_id,
{
"itemId": login_item["presentId"],
"itemKind": 6,
"stock": login_item["itemNum"],
"isValid": True,
},
all_login_boni = self.data.static.get_login_bonus(
self.version, preset["presetId"]
)
self.data.item.put_login_bonus(
user_id,
self.version,
preset["presetId"],
bonusCount=bonus_count,
lastUpdateDate=last_update_date,
isWatched=False,
isFinished=is_finished,
)
# skip the current bonus preset if no boni were found
if all_login_boni is None or len(all_login_boni) < 1:
self.logger.warn(
f"No bonus entries found for bonus preset {preset['presetId']}"
)
continue
max_needed_days = all_login_boni[0]["needLoginDayCount"]
# make sure to not show login boni after all days got redeemed
is_finished = False
if bonus_count > max_needed_days:
# assume that all login preset ids under 3000 needs to be
# looped, like 30 and 40 are looped, 40 does not work?
if preset["presetId"] < 3000:
bonus_count = 1
else:
is_finished = True
# grab the item for the corresponding day
login_item = self.data.static.get_login_bonus_by_required_days(
self.version, preset["presetId"], bonus_count
)
if login_item is not None:
# now add the present to the database so the
# handle_get_user_item_api_request can grab them
self.data.item.put_item(
user_id,
{
"itemId": login_item["presentId"],
"itemKind": 6,
"stock": login_item["itemNum"],
"isValid": True,
},
)
self.data.item.put_login_bonus(
user_id,
self.version,
preset["presetId"],
bonusCount=bonus_count,
lastUpdateDate=last_update_date,
isWatched=False,
isFinished=is_finished,
)
return {"returnCode": 1}
@ -639,97 +641,101 @@ class ChuniBase:
upsert = data["upsertUserAll"]
user_id = data["userId"]
if "userData" in upsert:
try:
upsert["userData"][0]["userName"] = self.read_wtf8(
upsert["userData"][0]["userName"]
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert:
try:
upsert["userData"][0]["userName"] = self.read_wtf8(
upsert["userData"][0]["userName"]
)
except Exception:
pass
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
except Exception:
pass
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userDataEx" in upsert:
self.data.profile.put_profile_data_ex(
user_id, self.version, upsert["userDataEx"][0]
)
if "userDataEx" in upsert:
self.data.profile.put_profile_data_ex(
user_id, self.version, upsert["userDataEx"][0]
)
if "userGameOption" in upsert:
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
if "userGameOption" in upsert:
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
if "userGameOptionEx" in upsert:
self.data.profile.put_profile_option_ex(
user_id, upsert["userGameOptionEx"][0]
)
if "userGameOptionEx" in upsert:
self.data.profile.put_profile_option_ex(
user_id, upsert["userGameOptionEx"][0]
)
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userCharacterList" in upsert:
for character in upsert["userCharacterList"]:
self.data.item.put_character(user_id, character)
if "userActivityList" in upsert:
for activity in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, activity)
if "userMapList" in upsert:
for map in upsert["userMapList"]:
self.data.item.put_map(user_id, map)
if "userChargeList" in upsert:
for charge in upsert["userChargeList"]:
self.data.profile.put_profile_charge(user_id, charge)
if "userCourseList" in upsert:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
if "userOverPowerList" in upsert:
for overpower in upsert["userOverPowerList"]:
self.data.profile.put_profile_overpower(user_id, overpower)
if "userDuelList" in upsert:
for duel in upsert["userDuelList"]:
self.data.item.put_duel(user_id, duel)
if "userEmoneyList" in upsert:
for emoney in upsert["userEmoneyList"]:
self.data.profile.put_profile_emoney(user_id, emoney)
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
with self.data.item.conn.begin() or nullcontext():
if "userCharacterList" in upsert:
for character in upsert["userCharacterList"]:
self.data.item.put_character(user_id, character)
if "userActivityList" in upsert:
for activity in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, activity)
if "userMapList" in upsert:
for map in upsert["userMapList"]:
self.data.item.put_map(user_id, map)
if "userChargeList" in upsert:
for charge in upsert["userChargeList"]:
self.data.profile.put_profile_charge(user_id, charge)
if "userDuelList" in upsert:
for duel in upsert["userDuelList"]:
self.data.item.put_duel(user_id, duel)
if "userMusicDetailList" in upsert:
for song in upsert["userMusicDetailList"]:
self.data.score.put_score(user_id, song)
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:
# convert the player names to utf-8
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
self.data.score.put_playlog(user_id, playlog)
if "userMapAreaList" in upsert:
for map_area in upsert["userMapAreaList"]:
self.data.item.put_map_area(user_id, map_area)
if "userLoginBonusList" in upsert:
for login in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id, self.version, login["presetId"], isWatched=True
)
with self.data.score.conn.begin() or nullcontext():
if "userCourseList" in upsert:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
if "userMusicDetailList" in upsert:
for song in upsert["userMusicDetailList"]:
self.data.score.put_score(user_id, song)
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:
# convert the player names to utf-8
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
self.data.score.put_playlog(user_id, playlog)
if "userTeamPoint" in upsert:
# TODO: team stuff
pass
if "userMapAreaList" in upsert:
for map_area in upsert["userMapAreaList"]:
self.data.item.put_map_area(user_id, map_area)
if "userOverPowerList" in upsert:
for overpower in upsert["userOverPowerList"]:
self.data.profile.put_profile_overpower(user_id, overpower)
if "userEmoneyList" in upsert:
for emoney in upsert["userEmoneyList"]:
self.data.profile.put_profile_emoney(user_id, emoney)
if "userLoginBonusList" in upsert:
for login in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id, self.version, login["presetId"], isWatched=True
)
return {"returnCode": "1"}
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:

View File

@ -33,7 +33,7 @@ class ChuniConstants:
"CHUNITHM PARADISE",
"CHUNITHM NEW!!",
"CHUNITHM NEW PLUS!!",
"CHUNITHM SUN"
"CHUNITHM SUN",
]
@classmethod

View File

@ -108,7 +108,9 @@ class ChuniServlet:
hmac_hash_module=SHA1,
)
hashed_name = hash.hex()[:32] # truncate unused bytes like the game does
hashed_name = hash.hex()[
:32
] # truncate unused bytes like the game does
self.hash_table[version][hashed_name] = method_fixed
self.logger.debug(

View File

@ -1,4 +1,5 @@
import logging
from contextlib import nullcontext
from datetime import datetime, timedelta
from random import randint
from typing import Dict
@ -375,22 +376,23 @@ class ChuniNew(ChuniBase):
user_gacha.pop("gachaId")
user_gacha.pop("dailyGachaDate")
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
with self.data.item.conn.begin() or nullcontext():
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
# save all user items
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
# save all user items
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
# add every gamegachaCard to database
for card in upsert["gameGachaCardList"]:
self.data.item.put_user_print_state(
user_id,
hasCompleted=False,
placeId=place_id,
cardId=card["cardId"],
gachaId=card["gachaId"],
)
# add every gamegachaCard to database
for card in upsert["gameGachaCardList"]:
self.data.item.put_user_print_state(
user_id,
hasCompleted=False,
placeId=place_id,
cardId=card["cardId"],
gachaId=card["gachaId"],
)
# retrieve every game gacha card which has been added in order to get
# the orderId for the next request
@ -451,14 +453,15 @@ class ChuniNew(ChuniBase):
place_id = data["placeId"]
# save all user items
if "userItemList" in data:
for item in data["userItemList"]:
self.data.item.put_item(user_id, item)
with self.data.item.conn.begin() or nullcontext():
if "userItemList" in data:
for item in data["userItemList"]:
self.data.item.put_item(user_id, item)
# set the card print state to success and use the orderId as the key
self.data.item.put_user_print_state(
user_id, id=upsert["orderId"], hasCompleted=True
)
# set the card print state to success and use the orderId as the key
self.data.item.put_user_print_state(
user_id, id=upsert["orderId"], hasCompleted=True
)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
@ -467,8 +470,9 @@ class ChuniNew(ChuniBase):
user_id = data["userId"]
# set the card print state to success and use the orderId as the key
for order_id in order_ids:
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
with self.data.item.conn.begin() or nullcontext():
for order_id in order_ids:
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
@ -481,40 +485,41 @@ class ChuniNew(ChuniBase):
# check if there is a free matching room
matching_room = self.data.item.get_oldest_free_matching(self.version)
if matching_room is None:
# grab the latest roomId and add 1 for the new room
newest_matching = self.data.item.get_newest_matching(self.version)
if newest_matching is not None:
room_id = newest_matching["roomId"] + 1
with self.data.item.conn.begin() or nullcontext():
if matching_room is None:
# grab the latest roomId and add 1 for the new room
newest_matching = self.data.item.get_newest_matching(self.version)
if newest_matching is not None:
room_id = newest_matching["roomId"] + 1
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
# create the new room with room_id and the current user id (host)
# user id is required for the countdown later on
self.data.item.put_matching(
self.version, room_id, [new_member], user_id=new_member["userId"]
)
# create the new room with room_id and the current user id (host)
# user id is required for the countdown later on
self.data.item.put_matching(
self.version, room_id, [new_member], user_id=new_member["userId"]
)
# get the newly created matching room
matching_room = self.data.item.get_matching(self.version, room_id)
else:
# a room already exists, so just add the new member to it
matching_member_list = matching_room["matchingMemberInfoList"]
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
matching_member_list.append(new_member)
# get the newly created matching room
matching_room = self.data.item.get_matching(self.version, room_id)
else:
# a room already exists, so just add the new member to it
matching_member_list = matching_room["matchingMemberInfoList"]
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
matching_member_list.append(new_member)
# add the updated room to the database, make sure to set isFull correctly!
self.data.item.put_matching(
self.version,
matching_room["roomId"],
matching_member_list,
user_id=matching_room["user"],
is_full=True if len(matching_member_list) >= 4 else False,
)
# add the updated room to the database, make sure to set isFull correctly!
self.data.item.put_matching(
self.version,
matching_room["roomId"],
matching_member_list,
user_id=matching_room["user"],
is_full=True if len(matching_member_list) >= 4 else False,
)
matching_wait = {
"isFinish": False,
@ -560,26 +565,27 @@ class ChuniNew(ChuniBase):
if matching_rooms is None:
return {"returnCode": "1"}
for room in matching_rooms:
old_members = room["matchingMemberInfoList"]
new_members = [m for m in old_members if m["userId"] != data["userId"]]
with self.data.item.conn.begin() or nullcontext():
for room in matching_rooms:
old_members = room["matchingMemberInfoList"]
new_members = [m for m in old_members if m["userId"] != data["userId"]]
# if nothing changed go to the next room
if len(old_members) == len(new_members):
continue
# if nothing changed go to the next room
if len(old_members) == len(new_members):
continue
# if the last user got removed, delete the matching room
if len(new_members) <= 0:
self.data.item.delete_matching(self.version, room["roomId"])
else:
# remove the user from the room
self.data.item.put_matching(
self.version,
room["roomId"],
new_members,
user_id=room["user"],
rest_sec=room["restMSec"],
)
# if the last user got removed, delete the matching room
if len(new_members) <= 0:
self.data.item.delete_matching(self.version, room["roomId"])
else:
# remove the user from the room
self.data.item.put_matching(
self.version,
room["roomId"],
new_members,
user_id=room["user"],
rest_sec=room["restMSec"],
)
return {"returnCode": "1"}

View File

@ -1,3 +1,4 @@
from contextlib import nullcontext
from typing import Optional
from os import walk, path
import xml.etree.ElementTree as ET
@ -38,11 +39,13 @@ class ChuniReader(BaseReader):
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
self.read_events(f"{dir}/event")
self.read_music(f"{dir}/music")
self.read_charges(f"{dir}/chargeItem")
self.read_avatar(f"{dir}/avatarAccessory")
self.read_login_bonus(f"{dir}/")
with self.data.static.conn.begin() or nullcontext():
self.read_events(f"{dir}/event")
self.read_music(f"{dir}/music")
self.read_charges(f"{dir}/chargeItem")
self.read_avatar(f"{dir}/avatarAccessory")
self.read_login_bonus(f"{dir}/")
def read_login_bonus(self, root_dir: str) -> None:
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):

View File

@ -1,6 +1,6 @@
from titles.chuni.schema.profile import ChuniProfileData
from titles.chuni.schema.score import ChuniScoreData
from titles.chuni.schema.item import ChuniItemData
from titles.chuni.schema.static import ChuniStaticData
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]
from titles.chuni.schema.profile import ChuniProfileData
from titles.chuni.schema.score import ChuniScoreData
from titles.chuni.schema.item import ChuniItemData
from titles.chuni.schema.static import ChuniStaticData
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,202 +1,202 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
course = Table(
"chuni_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("classId", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Boolean),
Column("scoreRank", Integer),
Column("eventId", Integer),
Column("lastPlayDate", String(25)),
Column("param1", Integer),
Column("param2", Integer),
Column("param3", Integer),
Column("param4", Integer),
Column("isClear", Boolean),
Column("theoryCount", Integer),
Column("orderId", Integer),
Column("playerRating", Integer),
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
mysql_charset="utf8mb4",
)
best_score = Table(
"chuni_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("resRequestCount", Integer),
Column("resAcceptCount", Integer),
Column("resSuccessCount", Integer),
Column("missCount", Integer),
Column("maxComboCount", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Boolean),
Column("fullChain", Integer),
Column("maxChain", Integer),
Column("scoreRank", Integer),
Column("isLock", Boolean),
Column("ext1", Integer),
Column("theoryCount", Integer),
UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"chuni_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("playDate", String(20)),
Column("userPlayDate", String(20)),
Column("musicId", Integer),
Column("level", Integer),
Column("customId", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(20)),
Column("playedUserName2", String(20)),
Column("playedUserName3", String(20)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("playedCustom1", Integer),
Column("playedCustom2", Integer),
Column("playedCustom3", Integer),
Column("track", Integer),
Column("score", Integer),
Column("rank", Integer),
Column("maxCombo", Integer),
Column("maxChain", Integer),
Column("rateTap", Integer),
Column("rateHold", Integer),
Column("rateSlide", Integer),
Column("rateAir", Integer),
Column("rateFlick", Integer),
Column("judgeGuilty", Integer),
Column("judgeAttack", Integer),
Column("judgeJustice", Integer),
Column("judgeCritical", Integer),
Column("eventId", Integer),
Column("playerRating", Integer),
Column("isNewRecord", Boolean),
Column("isFullCombo", Boolean),
Column("fullChainKind", Integer),
Column("isAllJustice", Boolean),
Column("isContinue", Boolean),
Column("isFreeToPlay", Boolean),
Column("characterId", Integer),
Column("skillId", Integer),
Column("playKind", Integer),
Column("isClear", Boolean),
Column("skillLevel", Integer),
Column("skillEffect", Integer),
Column("placeName", String(255)),
Column("isMaimai", Boolean),
Column("commonId", Integer),
Column("charaIllustId", Integer),
Column("romVersion", String(255)),
Column("judgeHeaven", Integer),
Column("regionId", Integer),
Column("machineType", Integer),
mysql_charset="utf8mb4"
)
class ChuniScoreData(BaseData):
def get_courses(self, aime_id: int) -> Optional[Row]:
sql = select(course).where(course.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = aime_id
course_data = self.fix_bools(course_data)
sql = insert(course).values(**course_data)
conflict = sql.on_duplicate_key_update(**course_data)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_scores(self, aime_id: int) -> Optional[Row]:
sql = select(best_score).where(best_score.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
score_data["user"] = aime_id
score_data = self.fix_bools(score_data)
sql = insert(best_score).values(**score_data)
conflict = sql.on_duplicate_key_update(**score_data)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_playlogs(self, aime_id: int) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]:
playlog_data["user"] = aime_id
playlog_data = self.fix_bools(playlog_data)
sql = insert(playlog).values(**playlog_data)
conflict = sql.on_duplicate_key_update(**playlog_data)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
course = Table(
"chuni_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("classId", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Boolean),
Column("scoreRank", Integer),
Column("eventId", Integer),
Column("lastPlayDate", String(25)),
Column("param1", Integer),
Column("param2", Integer),
Column("param3", Integer),
Column("param4", Integer),
Column("isClear", Boolean),
Column("theoryCount", Integer),
Column("orderId", Integer),
Column("playerRating", Integer),
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
mysql_charset="utf8mb4",
)
best_score = Table(
"chuni_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("resRequestCount", Integer),
Column("resAcceptCount", Integer),
Column("resSuccessCount", Integer),
Column("missCount", Integer),
Column("maxComboCount", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Boolean),
Column("fullChain", Integer),
Column("maxChain", Integer),
Column("scoreRank", Integer),
Column("isLock", Boolean),
Column("ext1", Integer),
Column("theoryCount", Integer),
UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"chuni_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("playDate", String(20)),
Column("userPlayDate", String(20)),
Column("musicId", Integer),
Column("level", Integer),
Column("customId", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(20)),
Column("playedUserName2", String(20)),
Column("playedUserName3", String(20)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("playedCustom1", Integer),
Column("playedCustom2", Integer),
Column("playedCustom3", Integer),
Column("track", Integer),
Column("score", Integer),
Column("rank", Integer),
Column("maxCombo", Integer),
Column("maxChain", Integer),
Column("rateTap", Integer),
Column("rateHold", Integer),
Column("rateSlide", Integer),
Column("rateAir", Integer),
Column("rateFlick", Integer),
Column("judgeGuilty", Integer),
Column("judgeAttack", Integer),
Column("judgeJustice", Integer),
Column("judgeCritical", Integer),
Column("eventId", Integer),
Column("playerRating", Integer),
Column("isNewRecord", Boolean),
Column("isFullCombo", Boolean),
Column("fullChainKind", Integer),
Column("isAllJustice", Boolean),
Column("isContinue", Boolean),
Column("isFreeToPlay", Boolean),
Column("characterId", Integer),
Column("skillId", Integer),
Column("playKind", Integer),
Column("isClear", Boolean),
Column("skillLevel", Integer),
Column("skillEffect", Integer),
Column("placeName", String(255)),
Column("isMaimai", Boolean),
Column("commonId", Integer),
Column("charaIllustId", Integer),
Column("romVersion", String(255)),
Column("judgeHeaven", Integer),
Column("regionId", Integer),
Column("machineType", Integer),
mysql_charset="utf8mb4",
)
class ChuniScoreData(BaseData):
def get_courses(self, aime_id: int) -> Optional[Row]:
sql = select(course).where(course.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = aime_id
course_data = self.fix_bools(course_data)
sql = insert(course).values(**course_data)
conflict = sql.on_conflict_do_update(set_=dict(**course_data))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_scores(self, aime_id: int) -> Optional[Row]:
sql = select(best_score).where(best_score.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
score_data["user"] = aime_id
score_data = self.fix_bools(score_data)
sql = insert(best_score).values(**score_data)
conflict = sql.on_conflict_do_update(set_=dict(**score_data))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_playlogs(self, aime_id: int) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]:
playlog_data["user"] = aime_id
playlog_data = self.fix_bools(playlog_data)
sql = insert(playlog).values(**playlog_data)
conflict = sql.on_conflict_do_update(set_=dict(**playlog_data))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid

File diff suppressed because it is too large Load Diff

View File

@ -13,8 +13,12 @@ class ChuniSun(ChuniNewPlus):
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
"rom"
]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
"data"
]
ret["gameSetting"][
"matchingUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"

View File

@ -30,7 +30,7 @@ class CardMakerServlet:
self.versions = [
CardMakerBase(core_cfg, self.game_cfg),
CardMaker135(core_cfg, self.game_cfg)
CardMaker135(core_cfg, self.game_cfg),
]
self.logger = logging.getLogger("cardmaker")

View File

@ -4,6 +4,7 @@ import os
import re
import csv
import xml.etree.ElementTree as ET
from contextlib import nullcontext
from typing import Any, Dict, List, Optional
from read import BaseReader
@ -78,10 +79,13 @@ class CardMakerReader(BaseReader):
# ONGEKI (MU3) cnnot easily access the bin data(A000.pac)
# so only opt_dir will work for now
for dir in data_dirs:
self.read_chuni_card(f"{dir}/CHU/card")
self.read_chuni_gacha(f"{dir}/CHU/gacha")
self.read_mai2_card(f"{dir}/MAI/card")
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
with self.chuni_data.static.conn.begin() or nullcontext():
self.read_chuni_card(f"{dir}/CHU/card")
self.read_chuni_gacha(f"{dir}/CHU/gacha")
with self.mai2_data.static.conn.begin() or nullcontext():
self.read_mai2_card(f"{dir}/MAI/card")
with self.ongeki_data.static.conn.begin() or nullcontext():
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
def read_chuni_card(self, base_dir: str) -> None:
self.logger.info(f"Reading cards from {base_dir}...")

View File

@ -1 +1 @@
__all__ = []
__all__ = []

View File

@ -1,6 +1,7 @@
import logging
import json
from decimal import Decimal
from contextlib import nullcontext
from base64 import b64encode
from typing import Any, Dict, List
from hashlib import md5
@ -13,6 +14,7 @@ from titles.cxb.database import CxbData
from threading import Thread
class CxbBase:
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
self.config = cfg # Config file
@ -255,8 +257,12 @@ class CxbBase:
# Async threads to generate the response
thread_Coupon = Thread(target=CxbBase.task_generateCoupon(index, data1))
thread_ShopListTitle = Thread(target=CxbBase.task_generateShopListTitle(index, data1))
thread_ShopListIcon = Thread(target=CxbBase.task_generateShopListIcon(index, data1))
thread_ShopListTitle = Thread(
target=CxbBase.task_generateShopListTitle(index, data1)
)
thread_ShopListIcon = Thread(
target=CxbBase.task_generateShopListIcon(index, data1)
)
thread_Stories = Thread(target=CxbBase.task_generateStories(index, data1))
thread_Coupon.start()
@ -270,11 +276,15 @@ class CxbBase:
thread_Stories.join()
for song in songs:
thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1))
thread_ScoreData = Thread(
target=CxbBase.task_generateScoreData(song, index, data1)
)
thread_ScoreData.start()
for v in index:
thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex))
thread_IndexData = Thread(
target=CxbBase.task_generateIndexData(versionindex)
)
thread_IndexData.start()
return {"index": index, "data": data1, "version": versionindex}
@ -295,50 +305,54 @@ class CxbBase:
)
# Alright.... time to bring the jank code
for value in data["saveindex"]["data"]:
if "playedUserId" in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "mcode" not in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "shopId" in value:
continue
if "mcode" in value[1] and "musicState" in value[1]:
song_json = json.loads(value[1])
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext()
):
for value in data["saveindex"]["data"]:
if "playedUserId" in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "mcode" not in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "shopId" in value:
continue
if "mcode" in value[1] and "musicState" in value[1]:
song_json = json.loads(value[1])
songCode = []
songCode.append(
{
"mcode": song_json["mcode"],
"musicState": song_json["musicState"],
"playCount": song_json["playCount"],
"totalScore": song_json["totalScore"],
"highScore": song_json["highScore"],
"clearRate": song_json["clearRate"],
"rankPoint": song_json["rankPoint"],
"combo": song_json["combo"],
"coupleUserId": song_json["coupleUserId"],
"difficulty": song_json["difficulty"],
"isFullCombo": song_json["isFullCombo"],
"clearGaugeType": song_json["clearGaugeType"],
"fieldType": song_json["fieldType"],
"gameType": song_json["gameType"],
"grade": song_json["grade"],
"unlockState": song_json["unlockState"],
"extraState": song_json["extraState"],
"index": value[0],
}
)
self.data.score.put_best_score(
data["saveindex"]["uid"],
song_json["mcode"],
self.version,
value[0],
songCode[0],
)
songCode = []
songCode.append(
{
"mcode": song_json["mcode"],
"musicState": song_json["musicState"],
"playCount": song_json["playCount"],
"totalScore": song_json["totalScore"],
"highScore": song_json["highScore"],
"clearRate": song_json["clearRate"],
"rankPoint": song_json["rankPoint"],
"combo": song_json["combo"],
"coupleUserId": song_json["coupleUserId"],
"difficulty": song_json["difficulty"],
"isFullCombo": song_json["isFullCombo"],
"clearGaugeType": song_json["clearGaugeType"],
"fieldType": song_json["fieldType"],
"gameType": song_json["gameType"],
"grade": song_json["grade"],
"unlockState": song_json["unlockState"],
"extraState": song_json["extraState"],
"index": value[0],
}
)
self.data.score.put_best_score(
data["saveindex"]["uid"],
song_json["mcode"],
self.version,
value[0],
songCode[0],
)
return {}
else:
self.logger.info(
@ -355,41 +369,42 @@ class CxbBase:
aimeId = profile["aimeId"]
i = 0
for index, value in enumerate(data["saveindex"]["data"]):
if int(data["saveindex"]["index"][index]) == 101:
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 700000
and int(data["saveindex"]["index"][index]) <= 701000
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 500
and int(data["saveindex"]["index"][index]) <= 510
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if "playedUserId" in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "mcode" not in value and "normalCR" not in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "shopId" in value:
continue
with self.data.profile.conn.begin() or nullcontext():
for index, value in enumerate(data["saveindex"]["data"]):
if int(data["saveindex"]["index"][index]) == 101:
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 700000
and int(data["saveindex"]["index"][index]) <= 701000
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 500
and int(data["saveindex"]["index"][index]) <= 510
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if "playedUserId" in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "mcode" not in value and "normalCR" not in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "shopId" in value:
continue
# MusicList Index for the profile
indexSongList = []
@ -397,47 +412,48 @@ class CxbBase:
if int(value) in range(100000, 110000):
indexSongList.append(value)
for index, value in enumerate(data["saveindex"]["data"]):
if "mcode" not in value:
continue
if "playedUserId" in value:
continue
with self.data.score.conn.begin() or nullcontext():
for index, value in enumerate(data["saveindex"]["data"]):
if "mcode" not in value:
continue
if "playedUserId" in value:
continue
data1 = json.loads(value)
data1 = json.loads(value)
songCode = []
songCode.append(
{
"mcode": data1["mcode"],
"musicState": data1["musicState"],
"playCount": data1["playCount"],
"totalScore": data1["totalScore"],
"highScore": data1["highScore"],
"everHighScore": data1["everHighScore"],
"clearRate": data1["clearRate"],
"rankPoint": data1["rankPoint"],
"normalCR": data1["normalCR"],
"survivalCR": data1["survivalCR"],
"ultimateCR": data1["ultimateCR"],
"nohopeCR": data1["nohopeCR"],
"combo": data1["combo"],
"coupleUserId": data1["coupleUserId"],
"difficulty": data1["difficulty"],
"isFullCombo": data1["isFullCombo"],
"clearGaugeType": data1["clearGaugeType"],
"fieldType": data1["fieldType"],
"gameType": data1["gameType"],
"grade": data1["grade"],
"unlockState": data1["unlockState"],
"extraState": data1["extraState"],
"index": indexSongList[i],
}
)
songCode = []
songCode.append(
{
"mcode": data1["mcode"],
"musicState": data1["musicState"],
"playCount": data1["playCount"],
"totalScore": data1["totalScore"],
"highScore": data1["highScore"],
"everHighScore": data1["everHighScore"],
"clearRate": data1["clearRate"],
"rankPoint": data1["rankPoint"],
"normalCR": data1["normalCR"],
"survivalCR": data1["survivalCR"],
"ultimateCR": data1["ultimateCR"],
"nohopeCR": data1["nohopeCR"],
"combo": data1["combo"],
"coupleUserId": data1["coupleUserId"],
"difficulty": data1["difficulty"],
"isFullCombo": data1["isFullCombo"],
"clearGaugeType": data1["clearGaugeType"],
"fieldType": data1["fieldType"],
"gameType": data1["gameType"],
"grade": data1["grade"],
"unlockState": data1["unlockState"],
"extraState": data1["extraState"],
"index": indexSongList[i],
}
)
self.data.score.put_best_score(
aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0]
)
i += 1
self.data.score.put_best_score(
aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0]
)
i += 1
return {}
def handle_action_sprankreq_request(self, data: Dict) -> Dict:
@ -485,43 +501,44 @@ class CxbBase:
uid = data["rankreg"]["uid"]
self.logger.info(f"Put {len(data['rankreg']['data'])} rankings for {uid}")
for rid in data["rankreg"]["data"]:
# REV S2
if "clear" in rid:
try:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=rid["clear"],
)
except Exception:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=rid["clear"],
)
# REV
else:
try:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=0,
)
except Exception:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=0,
)
with self.data.score.conn.begin() or nullcontext():
for rid in data["rankreg"]["data"]:
# REV S2
if "clear" in rid:
try:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=rid["clear"],
)
except Exception:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=rid["clear"],
)
# REV
else:
try:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=0,
)
except Exception:
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=0,
)
return {}
def handle_action_addenergy_request(self, data: Dict) -> Dict:
@ -532,20 +549,21 @@ class CxbBase:
p = self.data.item.get_energy(uid)
energy = p["energy"]
if not p:
self.data.item.put_energy(uid, 5)
with self.data.item.conn.begin() or nullcontext():
if not p:
self.data.item.put_energy(uid, 5)
return {
"class": data1["myClass"],
"granted": "5",
"total": "5",
"threshold": "1000",
}
return {
"class": data1["myClass"],
"granted": "5",
"total": "5",
"threshold": "1000",
}
array = []
array = []
newenergy = int(energy) + 5
self.data.item.put_energy(uid, newenergy)
newenergy = int(energy) + 5
self.data.item.put_energy(uid, newenergy)
if int(energy) <= 995:
array.append(
@ -573,4 +591,4 @@ class CxbBase:
def handle_action_stampreq_request(self, data: Dict) -> Dict:
self.logger.info(data)
return {"stampreq": ""}
return {"stampreq": ""}

View File

@ -1,5 +1,6 @@
from typing import Optional, Dict, List
from os import walk, path
from contextlib import nullcontext
import urllib
import csv
@ -37,7 +38,8 @@ class CxbReader(BaseReader):
pull_bin_ram = False
if pull_bin_ram:
self.read_csv(f"{self.bin_dir}")
with self.data.static.conn.begin() or nullcontext():
self.read_csv(f"{self.bin_dir}")
def read_csv(self, bin_dir: str) -> None:
self.logger.info(f"Read csv from {bin_dir}")

View File

@ -1,6 +1,6 @@
from titles.cxb.schema.profile import CxbProfileData
from titles.cxb.schema.score import CxbScoreData
from titles.cxb.schema.item import CxbItemData
from titles.cxb.schema.static import CxbStaticData
__all__ = [CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData]
from titles.cxb.schema.profile import CxbProfileData
from titles.cxb.schema.score import CxbScoreData
from titles.cxb.schema.item import CxbItemData
from titles.cxb.schema.static import CxbStaticData
__all__ = [CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData]

View File

@ -1,42 +1,42 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
energy = Table(
"cxb_rev_energy",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("energy", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", name="cxb_rev_energy_uk"),
mysql_charset="utf8mb4",
)
class CxbItemData(BaseData):
def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]:
sql = insert(energy).values(user=user_id, energy=rev_energy)
conflict = sql.on_duplicate_key_update(energy=rev_energy)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}"
)
return None
return result.lastrowid
def get_energy(self, user_id: int) -> Optional[Dict]:
sql = energy.select(and_(energy.c.user == user_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
energy = Table(
"cxb_rev_energy",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("energy", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", name="cxb_rev_energy_uk"),
mysql_charset="utf8mb4",
)
class CxbItemData(BaseData):
def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]:
sql = insert(energy).values(user=user_id, energy=rev_energy)
conflict = sql.on_conflict_do_update(set_=dict(energy=rev_energy))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}"
)
return None
return result.lastrowid
def get_energy(self, user_id: int) -> Optional[Dict]:
sql = energy.select(and_(energy.c.user == user_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,78 +1,78 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
profile = Table(
"cxb_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer, nullable=False),
Column("index", Integer, nullable=False),
Column("data", JSON, nullable=False),
UniqueConstraint("user", "index", name="cxb_profile_uk"),
mysql_charset="utf8mb4",
)
class CxbProfileData(BaseData):
def put_profile(
self, user_id: int, version: int, index: int, data: JSON
) -> Optional[int]:
sql = insert(profile).values(
user=user_id, version=version, index=index, data=data
)
conflict = sql.on_duplicate_key_update(index=index, data=data)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}"
)
return None
return result.lastrowid
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_profile_index(
self, index: int, aime_id: int = None, version: int = None
) -> Optional[Dict]:
"""
Given a game version and either a profile or aime id, return the profile
"""
if aime_id is not None and version is not None and index is not None:
sql = profile.select(
and_(
profile.c.version == version,
profile.c.user == aime_id,
profile.c.index == index,
)
)
else:
self.logger.error(
f"get_profile: Bad arguments!! aime_id {aime_id} version {version}"
)
return None
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
profile = Table(
"cxb_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer, nullable=False),
Column("index", Integer, nullable=False),
Column("data", JSON, nullable=False),
UniqueConstraint("user", "index", name="cxb_profile_uk"),
mysql_charset="utf8mb4",
)
class CxbProfileData(BaseData):
def put_profile(
self, user_id: int, version: int, index: int, data: JSON
) -> Optional[int]:
sql = insert(profile).values(
user=user_id, version=version, index=index, data=data
)
conflict = sql.on_conflict_do_update(set_=dict(index=index, data=data))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}"
)
return None
return result.lastrowid
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_profile_index(
self, index: int, aime_id: int = None, version: int = None
) -> Optional[Dict]:
"""
Given a game version and either a profile or aime id, return the profile
"""
if aime_id is not None and version is not None and index is not None:
sql = profile.select(
and_(
profile.c.version == version,
profile.c.user == aime_id,
profile.c.index == index,
)
)
else:
self.logger.error(
f"get_profile: Bad arguments!! aime_id {aime_id} version {version}"
)
return None
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,187 +1,187 @@
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import insert
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
score = Table(
"cxb_score",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("game_version", Integer),
Column("song_mcode", String(7)),
Column("song_index", Integer),
Column("data", JSON),
UniqueConstraint("user", "song_mcode", "song_index", name="cxb_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"cxb_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("song_mcode", String(7)),
Column("chart_id", Integer),
Column("score", Integer),
Column("clear", Integer),
Column("flawless", Integer),
Column("super", Integer),
Column("cool", Integer),
Column("fast", Integer),
Column("fast2", Integer),
Column("slow", Integer),
Column("slow2", Integer),
Column("fail", Integer),
Column("combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
ranking = Table(
"cxb_ranking",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("rev_id", Integer),
Column("song_id", Integer),
Column("score", Integer),
Column("clear", Integer),
UniqueConstraint("user", "rev_id", name="cxb_ranking_uk"),
mysql_charset="utf8mb4",
)
class CxbScoreData(BaseData):
def put_best_score(
self,
user_id: int,
song_mcode: str,
game_version: int,
song_index: int,
data: JSON,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
sql = insert(score).values(
user=user_id,
song_mcode=song_mcode,
game_version=game_version,
song_index=song_index,
data=data,
)
conflict = sql.on_duplicate_key_update(data=sql.inserted.data)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}"
)
return None
return result.lastrowid
def put_playlog(
self,
user_id: int,
song_mcode: str,
chart_id: int,
score: int,
clear: int,
flawless: int,
this_super: int,
cool: int,
this_fast: int,
this_fast2: int,
this_slow: int,
this_slow2: int,
fail: int,
combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
sql = playlog.insert().values(
user=user_id,
song_mcode=song_mcode,
chart_id=chart_id,
score=score,
clear=clear,
flawless=flawless,
super=this_super,
cool=cool,
fast=this_fast,
fast2=this_fast2,
slow=this_slow,
slow2=this_slow2,
fail=fail,
combo=combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}"
)
return None
return result.lastrowid
def put_ranking(
self, user_id: int, rev_id: int, song_id: int, score: int, clear: int
) -> Optional[int]:
"""
Add an entry to the user's ranking logs
"""
if song_id == 0:
sql = insert(ranking).values(
user=user_id, rev_id=rev_id, score=score, clear=clear
)
else:
sql = insert(ranking).values(
user=user_id, rev_id=rev_id, song_id=song_id, score=score, clear=clear
)
conflict = sql.on_duplicate_key_update(score=score)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}"
)
return None
return result.lastrowid
def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]:
sql = score.select(
and_(score.c.user == user_id, score.c.song_mcode == song_mcode)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[Dict]:
sql = score.select(score.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]:
sql = ranking.select(ranking.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.sqlite import insert
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
score = Table(
"cxb_score",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("game_version", Integer),
Column("song_mcode", String(7)),
Column("song_index", Integer),
Column("data", JSON),
UniqueConstraint("user", "song_mcode", "song_index", name="cxb_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"cxb_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("song_mcode", String(7)),
Column("chart_id", Integer),
Column("score", Integer),
Column("clear", Integer),
Column("flawless", Integer),
Column("super", Integer),
Column("cool", Integer),
Column("fast", Integer),
Column("fast2", Integer),
Column("slow", Integer),
Column("slow2", Integer),
Column("fail", Integer),
Column("combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
ranking = Table(
"cxb_ranking",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("rev_id", Integer),
Column("song_id", Integer),
Column("score", Integer),
Column("clear", Integer),
UniqueConstraint("user", "rev_id", name="cxb_ranking_uk"),
mysql_charset="utf8mb4",
)
class CxbScoreData(BaseData):
def put_best_score(
self,
user_id: int,
song_mcode: str,
game_version: int,
song_index: int,
data: JSON,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
sql = insert(score).values(
user=user_id,
song_mcode=song_mcode,
game_version=game_version,
song_index=song_index,
data=data,
)
conflict = sql.on_conflict_do_update(set_=dict(data=sql.inserted.data))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}"
)
return None
return result.lastrowid
def put_playlog(
self,
user_id: int,
song_mcode: str,
chart_id: int,
score: int,
clear: int,
flawless: int,
this_super: int,
cool: int,
this_fast: int,
this_fast2: int,
this_slow: int,
this_slow2: int,
fail: int,
combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
sql = playlog.insert().values(
user=user_id,
song_mcode=song_mcode,
chart_id=chart_id,
score=score,
clear=clear,
flawless=flawless,
super=this_super,
cool=cool,
fast=this_fast,
fast2=this_fast2,
slow=this_slow,
slow2=this_slow2,
fail=fail,
combo=combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}"
)
return None
return result.lastrowid
def put_ranking(
self, user_id: int, rev_id: int, song_id: int, score: int, clear: int
) -> Optional[int]:
"""
Add an entry to the user's ranking logs
"""
if song_id == 0:
sql = insert(ranking).values(
user=user_id, rev_id=rev_id, score=score, clear=clear
)
else:
sql = insert(ranking).values(
user=user_id, rev_id=rev_id, song_id=song_id, score=score, clear=clear
)
conflict = sql.on_conflict_do_update(set_=dict(score=score))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}"
)
return None
return result.lastrowid
def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]:
sql = score.select(
and_(score.c.user == user_id, score.c.song_mcode == song_mcode)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[Dict]:
sql = score.select(score.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]:
sql = ranking.select(ranking.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -1,95 +1,95 @@
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
music = Table(
"cxb_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", String(255)),
Column("index", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("category", String(255)),
Column("level", Float),
UniqueConstraint(
"version", "songId", "chartId", "index", name="cxb_static_music_uk"
),
mysql_charset="utf8mb4",
)
class CxbStaticData(BaseData):
def put_music(
self,
version: int,
mcode: str,
index: int,
chart: int,
title: str,
artist: str,
category: str,
level: float,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=mcode,
index=index,
chartId=chart,
title=title,
artist=artist,
category=category,
level=level,
)
conflict = sql.on_duplicate_key_update(
title=title, artist=artist, category=category, level=level
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
music = Table(
"cxb_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", String(255)),
Column("index", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("category", String(255)),
Column("level", Float),
UniqueConstraint(
"version", "songId", "chartId", "index", name="cxb_static_music_uk"
),
mysql_charset="utf8mb4",
)
class CxbStaticData(BaseData):
def put_music(
self,
version: int,
mcode: str,
index: int,
chart: int,
title: str,
artist: str,
category: str,
level: float,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=mcode,
index=index,
chartId=chart,
title=title,
artist=artist,
category=category,
level=level,
)
conflict = sql.on_conflict_do_update(
set_=dict(title=title, artist=artist, category=category, level=level)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -3,6 +3,7 @@ from typing import Any, List, Dict
import logging
import json
import urllib
from contextlib import nullcontext
from core.config import CoreConfig
from titles.diva.config import DivaConfig
@ -735,102 +736,103 @@ class DivaBase:
pd_song_worst_cnt = data["stg_wt_wg_cnt"].split(",")
pd_song_max_combo = data["stg_max_cmb"].split(",")
for index, value in enumerate(pd_song_list):
if "-1" not in pd_song_list[index]:
profile_pd_db_song = self.data.score.get_best_user_score(
data["pd_id"],
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
)
if profile_pd_db_song is None:
self.data.score.put_best_score(
with self.data.score.conn.begin() or nullcontext():
for index, value in enumerate(pd_song_list):
if "-1" not in pd_song_list[index]:
profile_pd_db_song = self.data.score.get_best_user_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]):
self.data.score.put_best_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]):
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
if profile_pd_db_song is None:
self.data.score.put_best_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]):
self.data.score.put_best_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]):
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
# Profile saving based on registration list

View File

@ -1,6 +1,7 @@
from typing import Optional, Dict, List
from os import walk, path
import urllib
from contextlib import nullcontext
from read import BaseReader
from core.config import CoreConfig
@ -47,13 +48,14 @@ class DivaReader(BaseReader):
pull_opt_rom = False
self.logger.warn("No option directory specified, skipping")
if pull_bin_ram:
self.read_ram(f"{self.bin_dir}/ram")
if pull_bin_rom:
self.read_rom(f"{self.bin_dir}/rom")
if pull_opt_rom:
for dir in opt_dirs:
self.read_rom(f"{dir}/rom")
with self.data.static.conn.begin() or nullcontext():
if pull_bin_ram:
self.read_ram(f"{self.bin_dir}/ram")
if pull_bin_rom:
self.read_rom(f"{self.bin_dir}/rom")
if pull_opt_rom:
for dir in opt_dirs:
self.read_rom(f"{dir}/rom")
def read_ram(self, ram_root_dir: str) -> None:
self.logger.info(f"Read RAM from {ram_root_dir}")

View File

@ -1,17 +1,17 @@
from titles.diva.schema.profile import DivaProfileData
from titles.diva.schema.score import DivaScoreData
from titles.diva.schema.module import DivaModuleData
from titles.diva.schema.customize import DivaCustomizeItemData
from titles.diva.schema.pv_customize import DivaPvCustomizeData
from titles.diva.schema.item import DivaItemData
from titles.diva.schema.static import DivaStaticData
__all__ = [
DivaProfileData,
DivaScoreData,
DivaModuleData,
DivaCustomizeItemData,
DivaPvCustomizeData,
DivaItemData,
DivaStaticData,
]
from titles.diva.schema.profile import DivaProfileData
from titles.diva.schema.score import DivaScoreData
from titles.diva.schema.module import DivaModuleData
from titles.diva.schema.customize import DivaCustomizeItemData
from titles.diva.schema.pv_customize import DivaPvCustomizeData
from titles.diva.schema.item import DivaItemData
from titles.diva.schema.static import DivaStaticData
__all__ = [
DivaProfileData,
DivaScoreData,
DivaModuleData,
DivaCustomizeItemData,
DivaPvCustomizeData,
DivaItemData,
DivaStaticData,
]

View File

@ -1,66 +1,66 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
customize = Table(
"diva_profile_customize_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("item_id", Integer, nullable=False),
UniqueConstraint(
"user", "version", "item_id", name="diva_profile_customize_item_uk"
),
mysql_charset="utf8mb4",
)
class DivaCustomizeItemData(BaseData):
def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None:
sql = insert(customize).values(version=version, user=aime_id, item_id=item_id)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}"
)
return None
return result.lastrowid
def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and an aime id, return all the customize items, not used directly
"""
sql = customize.select(
and_(customize.c.version == version, customize.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_customize_items_have_string(self, aime_id: int, version: int) -> str:
"""
Given a game version and an aime id, return the cstmz_itm_have hex string
required for diva directly
"""
items_list = self.get_customize_items(aime_id, version)
if items_list is None:
items_list = []
item_have = 0
for item in items_list:
item_have |= 1 << item["item_id"]
# convert the int to a 250 digit long hex string
return "{0:0>250}".format(hex(item_have).upper()[2:])
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
customize = Table(
"diva_profile_customize_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("item_id", Integer, nullable=False),
UniqueConstraint(
"user", "version", "item_id", name="diva_profile_customize_item_uk"
),
mysql_charset="utf8mb4",
)
class DivaCustomizeItemData(BaseData):
def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None:
sql = insert(customize).values(version=version, user=aime_id, item_id=item_id)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}"
)
return None
return result.lastrowid
def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and an aime id, return all the customize items, not used directly
"""
sql = customize.select(
and_(customize.c.version == version, customize.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_customize_items_have_string(self, aime_id: int, version: int) -> str:
"""
Given a game version and an aime id, return the cstmz_itm_have hex string
required for diva directly
"""
items_list = self.get_customize_items(aime_id, version)
if items_list is None:
items_list = []
item_have = 0
for item in items_list:
item_have |= 1 << item["item_id"]
# convert the int to a 250 digit long hex string
return "{0:0>250}".format(hex(item_have).upper()[2:])

View File

@ -1,68 +1,70 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
shop = Table(
"diva_profile_shop",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("mdl_eqp_ary", String(32)),
Column("c_itm_eqp_ary", String(59)),
Column("ms_itm_flg_ary", String(59)),
UniqueConstraint("user", "version", name="diva_profile_shop_uk"),
mysql_charset="utf8mb4",
)
class DivaItemData(BaseData):
def put_shop(
self,
aime_id: int,
version: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> None:
sql = insert(shop).values(
version=version,
user=aime_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
conflict = sql.on_duplicate_key_update(
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}"
)
return None
return result.lastrowid
def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
shop = Table(
"diva_profile_shop",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("mdl_eqp_ary", String(32)),
Column("c_itm_eqp_ary", String(59)),
Column("ms_itm_flg_ary", String(59)),
UniqueConstraint("user", "version", name="diva_profile_shop_uk"),
mysql_charset="utf8mb4",
)
class DivaItemData(BaseData):
def put_shop(
self,
aime_id: int,
version: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> None:
sql = insert(shop).values(
version=version,
user=aime_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
conflict = sql.on_conflict_do_update(
set_=dict(
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}"
)
return None
return result.lastrowid
def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,62 +1,62 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
module = Table(
"diva_profile_module",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("module_id", Integer, nullable=False),
UniqueConstraint("user", "version", "module_id", name="diva_profile_module_uk"),
mysql_charset="utf8mb4",
)
class DivaModuleData(BaseData):
def put_module(self, aime_id: int, version: int, module_id: int) -> None:
sql = insert(module).values(version=version, user=aime_id, module_id=module_id)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}"
)
return None
return result.lastrowid
def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and an aime id, return all the modules, not used directly
"""
sql = module.select(and_(module.c.version == version, module.c.user == aime_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_modules_have_string(self, aime_id: int, version: int) -> str:
"""
Given a game version and an aime id, return the mdl_have hex string
required for diva directly
"""
module_list = self.get_modules(aime_id, version)
if module_list is None:
module_list = []
module_have = 0
for module in module_list:
module_have |= 1 << module["module_id"]
# convert the int to a 250 digit long hex string
return "{0:0>250}".format(hex(module_have).upper()[2:])
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
module = Table(
"diva_profile_module",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("module_id", Integer, nullable=False),
UniqueConstraint("user", "version", "module_id", name="diva_profile_module_uk"),
mysql_charset="utf8mb4",
)
class DivaModuleData(BaseData):
def put_module(self, aime_id: int, version: int, module_id: int) -> None:
sql = insert(module).values(version=version, user=aime_id, module_id=module_id)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}"
)
return None
return result.lastrowid
def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and an aime id, return all the modules, not used directly
"""
sql = module.select(and_(module.c.version == version, module.c.user == aime_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_modules_have_string(self, aime_id: int, version: int) -> str:
"""
Given a game version and an aime id, return the mdl_have hex string
required for diva directly
"""
module_list = self.get_modules(aime_id, version)
if module_list is None:
module_list = []
module_have = 0
for module in module_list:
module_have |= 1 << module["module_id"]
# convert the int to a 250 digit long hex string
return "{0:0>250}".format(hex(module_have).upper()[2:])

View File

@ -1,117 +1,119 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
profile = Table(
"diva_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("player_name", String(10), nullable=False),
Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"),
Column("lv_num", Integer, nullable=False, server_default="0"),
Column("lv_pnt", Integer, nullable=False, server_default="0"),
Column("vcld_pts", Integer, nullable=False, server_default="0"),
Column("hp_vol", Integer, nullable=False, server_default="100"),
Column("btn_se_vol", Integer, nullable=False, server_default="100"),
Column("btn_se_vol2", Integer, nullable=False, server_default="100"),
Column("sldr_se_vol2", Integer, nullable=False, server_default="100"),
Column("sort_kind", Integer, nullable=False, server_default="2"),
Column("use_pv_mdl_eqp", Boolean, nullable=False, server_default="1"),
Column("use_mdl_pri", Boolean, nullable=False, server_default="0"),
Column("use_pv_skn_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_btn_se_eqp", Boolean, nullable=False, server_default="1"),
Column("use_pv_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_chn_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_sldr_tch_se_eqp", Boolean, nullable=False, server_default="0"),
Column("btn_se_eqp", Integer, nullable=False, server_default="-1"),
Column("sld_se_eqp", Integer, nullable=False, server_default="-1"),
Column("chn_sld_se_eqp", Integer, nullable=False, server_default="-1"),
Column("sldr_tch_se_eqp", Integer, nullable=False, server_default="-1"),
Column("nxt_pv_id", Integer, nullable=False, server_default="708"),
Column("nxt_dffclty", Integer, nullable=False, server_default="2"),
Column("nxt_edtn", Integer, nullable=False, server_default="0"),
Column("cnp_cid", Integer, nullable=False, server_default="-1"),
Column("cnp_val", Integer, nullable=False, server_default="-1"),
Column("cnp_rr", Integer, nullable=False, server_default="-1"),
Column("cnp_sp", String(255), nullable=False, server_default=""),
Column("dsp_clr_brdr", Integer, nullable=False, server_default="7"),
Column("dsp_intrm_rnk", Integer, nullable=False, server_default="1"),
Column("dsp_clr_sts", Integer, nullable=False, server_default="1"),
Column("rgo_sts", Integer, nullable=False, server_default="1"),
Column("lv_efct_id", Integer, nullable=False, server_default="0"),
Column("lv_plt_id", Integer, nullable=False, server_default="1"),
Column("passwd_stat", Integer, nullable=False, server_default="0"),
Column("passwd", String(12), nullable=False, server_default="**********"),
Column(
"my_qst_id",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column(
"my_qst_sts",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
UniqueConstraint("user", "version", name="diva_profile_uk"),
mysql_charset="utf8mb4",
)
class DivaProfileData(BaseData):
def create_profile(
self, version: int, aime_id: int, player_name: str
) -> Optional[int]:
"""
Given a game version, aime id, and player_name, create a profile and return it's ID
"""
sql = insert(profile).values(
version=version, user=aime_id, player_name=player_name
)
conflict = sql.on_duplicate_key_update(player_name=sql.inserted.player_name)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}"
)
return None
return result.lastrowid
def update_profile(self, aime_id: int, **profile_args) -> None:
"""
Given an aime_id update the profile corresponding to the arguments
which are the diva_profile Columns
"""
sql = profile.update(profile.c.user == aime_id).values(**profile_args)
result = self.execute(sql)
if result is None:
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}"
)
return None
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
profile = Table(
"diva_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("player_name", String(10), nullable=False),
Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"),
Column("lv_num", Integer, nullable=False, server_default="0"),
Column("lv_pnt", Integer, nullable=False, server_default="0"),
Column("vcld_pts", Integer, nullable=False, server_default="0"),
Column("hp_vol", Integer, nullable=False, server_default="100"),
Column("btn_se_vol", Integer, nullable=False, server_default="100"),
Column("btn_se_vol2", Integer, nullable=False, server_default="100"),
Column("sldr_se_vol2", Integer, nullable=False, server_default="100"),
Column("sort_kind", Integer, nullable=False, server_default="2"),
Column("use_pv_mdl_eqp", Boolean, nullable=False, server_default="1"),
Column("use_mdl_pri", Boolean, nullable=False, server_default="0"),
Column("use_pv_skn_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_btn_se_eqp", Boolean, nullable=False, server_default="1"),
Column("use_pv_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_chn_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_sldr_tch_se_eqp", Boolean, nullable=False, server_default="0"),
Column("btn_se_eqp", Integer, nullable=False, server_default="-1"),
Column("sld_se_eqp", Integer, nullable=False, server_default="-1"),
Column("chn_sld_se_eqp", Integer, nullable=False, server_default="-1"),
Column("sldr_tch_se_eqp", Integer, nullable=False, server_default="-1"),
Column("nxt_pv_id", Integer, nullable=False, server_default="708"),
Column("nxt_dffclty", Integer, nullable=False, server_default="2"),
Column("nxt_edtn", Integer, nullable=False, server_default="0"),
Column("cnp_cid", Integer, nullable=False, server_default="-1"),
Column("cnp_val", Integer, nullable=False, server_default="-1"),
Column("cnp_rr", Integer, nullable=False, server_default="-1"),
Column("cnp_sp", String(255), nullable=False, server_default=""),
Column("dsp_clr_brdr", Integer, nullable=False, server_default="7"),
Column("dsp_intrm_rnk", Integer, nullable=False, server_default="1"),
Column("dsp_clr_sts", Integer, nullable=False, server_default="1"),
Column("rgo_sts", Integer, nullable=False, server_default="1"),
Column("lv_efct_id", Integer, nullable=False, server_default="0"),
Column("lv_plt_id", Integer, nullable=False, server_default="1"),
Column("passwd_stat", Integer, nullable=False, server_default="0"),
Column("passwd", String(12), nullable=False, server_default="**********"),
Column(
"my_qst_id",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column(
"my_qst_sts",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
UniqueConstraint("user", "version", name="diva_profile_uk"),
mysql_charset="utf8mb4",
)
class DivaProfileData(BaseData):
def create_profile(
self, version: int, aime_id: int, player_name: str
) -> Optional[int]:
"""
Given a game version, aime id, and player_name, create a profile and return it's ID
"""
sql = insert(profile).values(
version=version, user=aime_id, player_name=player_name
)
conflict = sql.on_conflict_do_update(
set_=dict(player_name=sql.inserted.player_name)
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}"
)
return None
return result.lastrowid
def update_profile(self, aime_id: int, **profile_args) -> None:
"""
Given an aime_id update the profile corresponding to the arguments
which are the diva_profile Columns
"""
sql = profile.update(profile.c.user == aime_id).values(**profile_args)
result = self.execute(sql)
if result is None:
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}"
)
return None
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,86 +1,88 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer, String
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
pv_customize = Table(
"diva_profile_pv_customize",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("pv_id", Integer, nullable=False),
Column("mdl_eqp_ary", String(14), server_default="-999,-999,-999"),
Column(
"c_itm_eqp_ary",
String(59),
server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999",
),
Column(
"ms_itm_flg_ary",
String(59),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column("skin", Integer, server_default="-1"),
Column("btn_se", Integer, server_default="-1"),
Column("sld_se", Integer, server_default="-1"),
Column("chsld_se", Integer, server_default="-1"),
Column("sldtch_se", Integer, server_default="-1"),
UniqueConstraint("user", "version", "pv_id", name="diva_profile_pv_customize_uk"),
mysql_charset="utf8mb4",
)
class DivaPvCustomizeData(BaseData):
def put_pv_customize(
self,
aime_id: int,
version: int,
pv_id: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> Optional[int]:
sql = insert(pv_customize).values(
version=version,
user=aime_id,
pv_id=pv_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
conflict = sql.on_duplicate_key_update(
pv_id=pv_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}"
)
return None
return result.lastrowid
def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]:
"""
Given either a profile or aime id, return a Pv Customize row
"""
sql = pv_customize.select(
and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer, String
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
pv_customize = Table(
"diva_profile_pv_customize",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("pv_id", Integer, nullable=False),
Column("mdl_eqp_ary", String(14), server_default="-999,-999,-999"),
Column(
"c_itm_eqp_ary",
String(59),
server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999",
),
Column(
"ms_itm_flg_ary",
String(59),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column("skin", Integer, server_default="-1"),
Column("btn_se", Integer, server_default="-1"),
Column("sld_se", Integer, server_default="-1"),
Column("chsld_se", Integer, server_default="-1"),
Column("sldtch_se", Integer, server_default="-1"),
UniqueConstraint("user", "version", "pv_id", name="diva_profile_pv_customize_uk"),
mysql_charset="utf8mb4",
)
class DivaPvCustomizeData(BaseData):
def put_pv_customize(
self,
aime_id: int,
version: int,
pv_id: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> Optional[int]:
sql = insert(pv_customize).values(
version=version,
user=aime_id,
pv_id=pv_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
conflict = sql.on_conflict_do_update(
set_=dict(
pv_id=pv_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary,
)
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}"
)
return None
return result.lastrowid
def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]:
"""
Given either a profile or aime id, return a Pv Customize row
"""
sql = pv_customize.select(
and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,241 +1,243 @@
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.engine import Row
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
score = Table(
"diva_score",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer),
Column("pv_id", Integer),
Column("difficulty", Integer),
Column("edition", Integer),
Column("score", Integer),
Column("atn_pnt", Integer),
Column("clr_kind", Integer),
Column("sort_kind", Integer),
Column("cool", Integer),
Column("fine", Integer),
Column("safe", Integer),
Column("sad", Integer),
Column("worst", Integer),
Column("max_combo", Integer),
UniqueConstraint("user", "pv_id", "difficulty", "edition", name="diva_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"diva_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer),
Column("pv_id", Integer),
Column("difficulty", Integer),
Column("edition", Integer),
Column("score", Integer),
Column("atn_pnt", Integer),
Column("clr_kind", Integer),
Column("sort_kind", Integer),
Column("cool", Integer),
Column("fine", Integer),
Column("safe", Integer),
Column("sad", Integer),
Column("worst", Integer),
Column("max_combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
class DivaScoreData(BaseData):
def put_best_score(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
sql = insert(score).values(
user=user_id,
version=game_version,
pv_id=song_id,
difficulty=difficulty,
edition=edition,
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
conflict = sql.on_duplicate_key_update(
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}"
)
return None
return result.lastrowid
def put_playlog(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
sql = playlog.insert().values(
user=user_id,
version=game_version,
pv_id=song_id,
difficulty=difficulty,
edition=edition,
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}"
)
return None
return result.lastrowid
def get_best_user_score(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[Row]:
sql = score.select(
and_(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_top3_scores(
self, pv_id: int, difficulty: int, edition: int
) -> Optional[List[Row]]:
sql = (
score.select(
and_(
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
.order_by(score.c.score.desc())
.limit(3)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_global_ranking(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[List[Row]]:
# get the subquery max score of a user with pv_id, difficulty and
# edition
sql_sub = (
select([score.c.score])
.filter(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
.scalar_subquery()
)
# Perform the main query, also rename the resulting column to ranking
sql = select(func.count(score.c.id).label("ranking")).filter(
score.c.score >= sql_sub,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[List[Row]]:
sql = score.select(score.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.engine import Row
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
score = Table(
"diva_score",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer),
Column("pv_id", Integer),
Column("difficulty", Integer),
Column("edition", Integer),
Column("score", Integer),
Column("atn_pnt", Integer),
Column("clr_kind", Integer),
Column("sort_kind", Integer),
Column("cool", Integer),
Column("fine", Integer),
Column("safe", Integer),
Column("sad", Integer),
Column("worst", Integer),
Column("max_combo", Integer),
UniqueConstraint("user", "pv_id", "difficulty", "edition", name="diva_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"diva_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("version", Integer),
Column("pv_id", Integer),
Column("difficulty", Integer),
Column("edition", Integer),
Column("score", Integer),
Column("atn_pnt", Integer),
Column("clr_kind", Integer),
Column("sort_kind", Integer),
Column("cool", Integer),
Column("fine", Integer),
Column("safe", Integer),
Column("sad", Integer),
Column("worst", Integer),
Column("max_combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
class DivaScoreData(BaseData):
def put_best_score(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
sql = insert(score).values(
user=user_id,
version=game_version,
pv_id=song_id,
difficulty=difficulty,
edition=edition,
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
conflict = sql.on_conflict_do_update(
set_=dict(
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}"
)
return None
return result.lastrowid
def put_playlog(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
sql = playlog.insert().values(
user=user_id,
version=game_version,
pv_id=song_id,
difficulty=difficulty,
edition=edition,
score=song_score,
atn_pnt=atn_pnt,
clr_kind=clr_kind,
sort_kind=sort_kind,
cool=cool,
fine=fine,
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}"
)
return None
return result.lastrowid
def get_best_user_score(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[Row]:
sql = score.select(
and_(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_top3_scores(
self, pv_id: int, difficulty: int, edition: int
) -> Optional[List[Row]]:
sql = (
score.select(
and_(
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
.order_by(score.c.score.desc())
.limit(3)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_global_ranking(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[List[Row]]:
# get the subquery max score of a user with pv_id, difficulty and
# edition
sql_sub = (
select([score.c.score])
.filter(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
.scalar_subquery()
)
# Perform the main query, also rename the resulting column to ranking
sql = select(func.count(score.c.id).label("ranking")).filter(
score.c.score >= sql_sub,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[List[Row]]:
sql = score.select(score.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -1,311 +1,313 @@
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
music = Table(
"diva_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("vocaloid_arranger", String(255)),
Column("pv_illustrator", String(255)),
Column("lyrics", String(255)),
Column("bg_music", String(255)),
Column("level", Float),
Column("bpm", Integer),
Column("date", String(255)),
UniqueConstraint("version", "songId", "chartId", name="diva_static_music_uk"),
mysql_charset="utf8mb4",
)
quests = Table(
"diva_static_quests",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("questId", Integer),
Column("name", String(255)),
Column("quest_enable", Boolean, server_default="1"),
Column("kind", Integer),
Column("unknown_0", Integer),
Column("unknown_1", Integer),
Column("unknown_2", Integer),
Column("quest_order", Integer),
Column("start_datetime", String(255)),
Column("end_datetime", String(255)),
UniqueConstraint("version", "questId", name="diva_static_quests_uk"),
mysql_charset="utf8mb4",
)
shop = Table(
"diva_static_shop",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("shopId", Integer),
Column("name", String(255)),
Column("type", Integer),
Column("points", Integer),
Column("unknown_0", Integer),
Column("start_date", String(255)),
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "shopId", name="diva_static_shop_uk"),
mysql_charset="utf8mb4",
)
items = Table(
"diva_static_items",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("itemId", Integer),
Column("name", String(255)),
Column("type", Integer),
Column("points", Integer),
Column("unknown_0", Integer),
Column("start_date", String(255)),
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "itemId", name="diva_static_items_uk"),
mysql_charset="utf8mb4",
)
class DivaStaticData(BaseData):
def put_quests(
self,
version: int,
questId: int,
name: str,
kind: int,
unknown_0: int,
unknown_1: int,
unknown_2: int,
quest_order: int,
start_datetime: str,
end_datetime: str,
) -> Optional[int]:
sql = insert(quests).values(
version=version,
questId=questId,
name=name,
kind=kind,
unknown_0=unknown_0,
unknown_1=unknown_1,
unknown_2=unknown_2,
quest_order=quest_order,
start_datetime=start_datetime,
end_datetime=end_datetime,
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_quests(self, version: int) -> Optional[List[Row]]:
sql = select(quests).where(
and_(quests.c.version == version, quests.c.quest_enable == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_shop(
self,
version: int,
shopId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(shop).values(
version=version,
shopId=shopId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]:
sql = select(shop).where(
and_(
shop.c.version == version,
shop.c.shopId == shopId,
shop.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_enabled_shops(self, version: int) -> Optional[List[Row]]:
sql = select(shop).where(
and_(shop.c.version == version, shop.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_items(
self,
version: int,
itemId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(items).values(
version=version,
itemId=itemId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]:
sql = select(items).where(
and_(
items.c.version == version,
items.c.itemId == itemId,
items.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_enabled_items(self, version: int) -> Optional[List[Row]]:
sql = select(items).where(
and_(items.c.version == version, items.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_music(
self,
version: int,
song: int,
chart: int,
title: str,
arranger: str,
illustrator: str,
lyrics: str,
music_comp: str,
level: float,
bpm: int,
date: str,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=song,
chartId=chart,
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
conflict = sql.on_duplicate_key_update(
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
music = Table(
"diva_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("vocaloid_arranger", String(255)),
Column("pv_illustrator", String(255)),
Column("lyrics", String(255)),
Column("bg_music", String(255)),
Column("level", Float),
Column("bpm", Integer),
Column("date", String(255)),
UniqueConstraint("version", "songId", "chartId", name="diva_static_music_uk"),
mysql_charset="utf8mb4",
)
quests = Table(
"diva_static_quests",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("questId", Integer),
Column("name", String(255)),
Column("quest_enable", Boolean, server_default="1"),
Column("kind", Integer),
Column("unknown_0", Integer),
Column("unknown_1", Integer),
Column("unknown_2", Integer),
Column("quest_order", Integer),
Column("start_datetime", String(255)),
Column("end_datetime", String(255)),
UniqueConstraint("version", "questId", name="diva_static_quests_uk"),
mysql_charset="utf8mb4",
)
shop = Table(
"diva_static_shop",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("shopId", Integer),
Column("name", String(255)),
Column("type", Integer),
Column("points", Integer),
Column("unknown_0", Integer),
Column("start_date", String(255)),
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "shopId", name="diva_static_shop_uk"),
mysql_charset="utf8mb4",
)
items = Table(
"diva_static_items",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("itemId", Integer),
Column("name", String(255)),
Column("type", Integer),
Column("points", Integer),
Column("unknown_0", Integer),
Column("start_date", String(255)),
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "itemId", name="diva_static_items_uk"),
mysql_charset="utf8mb4",
)
class DivaStaticData(BaseData):
def put_quests(
self,
version: int,
questId: int,
name: str,
kind: int,
unknown_0: int,
unknown_1: int,
unknown_2: int,
quest_order: int,
start_datetime: str,
end_datetime: str,
) -> Optional[int]:
sql = insert(quests).values(
version=version,
questId=questId,
name=name,
kind=kind,
unknown_0=unknown_0,
unknown_1=unknown_1,
unknown_2=unknown_2,
quest_order=quest_order,
start_datetime=start_datetime,
end_datetime=end_datetime,
)
conflict = sql.on_conflict_do_update(set_=dict(name=name))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_quests(self, version: int) -> Optional[List[Row]]:
sql = select(quests).where(
and_(quests.c.version == version, quests.c.quest_enable == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_shop(
self,
version: int,
shopId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(shop).values(
version=version,
shopId=shopId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_conflict_do_update(set_=dict(name=name))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]:
sql = select(shop).where(
and_(
shop.c.version == version,
shop.c.shopId == shopId,
shop.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_enabled_shops(self, version: int) -> Optional[List[Row]]:
sql = select(shop).where(
and_(shop.c.version == version, shop.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_items(
self,
version: int,
itemId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(items).values(
version=version,
itemId=itemId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_conflict_do_update(set_=dict(name=name))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]:
sql = select(items).where(
and_(
items.c.version == version,
items.c.itemId == itemId,
items.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_enabled_items(self, version: int) -> Optional[List[Row]]:
sql = select(items).where(
and_(items.c.version == version, items.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_music(
self,
version: int,
song: int,
chart: int,
title: str,
arranger: str,
illustrator: str,
lyrics: str,
music_comp: str,
level: float,
bpm: int,
date: str,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=song,
chartId=chart,
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
conflict = sql.on_conflict_do_update(
set_=dict(
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -83,13 +83,15 @@ class IDZUserDBProtocol(Protocol):
def dataReceived(self, data: bytes) -> None:
self.logger.debug(f"Receive data {data.hex()}")
crypt = AES.new(self.static_key, AES.MODE_ECB)
try:
data_dec = crypt.decrypt(data)
except Exception as e:
self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}")
self.logger.error(
f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}"
)
self.logger.debug(f"Decrypt data {data_dec.hex()}")
magic = struct.unpack_from("<I", data_dec, 0)[0]

View File

@ -4,6 +4,7 @@ import logging
from base64 import b64decode
from os import path, stat, remove
from PIL import ImageFile
from contextlib import nullcontext
from core.config import CoreConfig
from titles.mai2.const import Mai2Constants
@ -21,15 +22,15 @@ class Mai2Base:
self.can_deliver = False
self.can_usbdl = False
self.old_server = ""
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/"
def handle_get_game_setting_api_request(self, data: Dict):
return {
return {
"isDevelop": False,
"isAouAccession": False,
"gameSetting": {
@ -39,10 +40,16 @@ class Mai2Base:
"rebootEndTime": "2020-01-01 07:59:59.0",
"movieUploadLimit": 100,
"movieStatus": 1,
"movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie",
"deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "",
"movieServerUri": self.old_server + "api/movie"
if self.game_config.uploads.movies
else "movie",
"deliverServerUri": self.old_server + "deliver/"
if self.can_deliver and self.game_config.deliver.enable
else "",
"oldServerUri": self.old_server + "old",
"usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "",
"usbDlServerUri": self.old_server + "usbdl/"
if self.can_deliver and self.game_config.deliver.udbdl_enable
else "",
},
}
@ -154,32 +161,37 @@ class Mai2Base:
else:
loginCt = 0
lastLoginDate = "2017-12-05 07:00:00.0"
if consec is None or not consec:
consec_ct = 1
else:
lastlogindate_ = datetime.strptime(profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f").timestamp()
today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()
lastlogindate_ = datetime.strptime(
profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f"
).timestamp()
today_midnight = (
datetime.now()
.replace(hour=0, minute=0, second=0, microsecond=0)
.timestamp()
)
yesterday_midnight = today_midnight - 86400
if lastlogindate_ < today_midnight:
consec_ct = consec['logins'] + 1
consec_ct = consec["logins"] + 1
self.data.profile.add_consec_login(data["userId"], self.version)
elif lastlogindate_ < yesterday_midnight:
consec_ct = 1
self.data.profile.reset_consec_login(data["userId"], self.version)
else:
consec_ct = consec['logins']
consec_ct = consec["logins"]
return {
"returnCode": 1,
"lastLoginDate": lastLoginDate,
"loginCount": loginCt,
"consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in.
"consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in.
}
def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict:
@ -210,140 +222,132 @@ class Mai2Base:
user_id = data["userId"]
upsert = data["upsertUserAll"]
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0].pop("accessCode")
upsert["userData"][0].pop("userId")
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0].pop("accessCode")
upsert["userData"][0].pop("userId")
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0], False
)
if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0:
upsert["userWebOption"][0]["isNetMember"] = True
self.data.profile.put_web_option(
user_id, self.version, upsert["userWebOption"][0]
)
if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0:
self.data.profile.put_grade_status(
user_id, upsert["userGradeStatusList"][0]
)
if "userBossList" in upsert and len(upsert["userBossList"]) > 0:
self.data.profile.put_boss_list(
user_id, upsert["userBossList"][0]
)
if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(
user_id, playlog, False
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0], False
)
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_ghost(user_id, self.version, ghost)
if "userRecentRatingList" in upsert:
self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"])
if "userOption" in upsert and len(upsert["userOption"]) > 0:
upsert["userOption"][0].pop("userId")
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0], False
)
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, act)
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
for charge in upsert["userChargeList"]:
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
),
datetime.strptime(
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
),
if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0:
upsert["userWebOption"][0]["isNetMember"] = True
self.data.profile.put_web_option(
user_id, self.version, upsert["userWebOption"][0]
)
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character_(
user_id,
char
if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0:
self.data.profile.put_grade_status(
user_id, upsert["userGradeStatusList"][0]
)
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item["stock"],
True
if "userBossList" in upsert and len(upsert["userBossList"]) > 0:
self.data.profile.put_boss_list(user_id, upsert["userBossList"][0])
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_ghost(user_id, self.version, ghost)
if "userRecentRatingList" in upsert:
self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"])
if "userOption" in upsert and len(upsert["userOption"]) > 0:
upsert["userOption"][0].pop("userId")
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0], False
)
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music, False)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, act)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
with self.data.score.conn.begin() or nullcontext():
if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(user_id, playlog, False)
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music, False)
if (
"userFriendSeasonRankingList" in upsert
and len(upsert["userFriendSeasonRankingList"]) > 0
):
for fsr in upsert["userFriendSeasonRankingList"]:
fsr["recordDate"] = (
datetime.strptime(
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
),
)
self.data.item.put_friend_season_ranking(user_id, fsr)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
with self.data.item.conn.begin() or nullcontext():
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
for charge in upsert["userChargeList"]:
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
),
datetime.strptime(
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
),
)
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character_(user_id, char)
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id, int(item["itemKind"]), item["itemId"], item["stock"], True
)
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
)
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
)
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
if (
"userFriendSeasonRankingList" in upsert
and len(upsert["userFriendSeasonRankingList"]) > 0
):
for fsr in upsert["userFriendSeasonRankingList"]:
fsr["recordDate"] = (
datetime.strptime(
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
),
)
self.data.item.put_friend_season_ranking(user_id, fsr)
return {"returnCode": 1, "apiName": "UpsertUserAllApi"}
@ -351,7 +355,9 @@ class Mai2Base:
return {"returnCode": 1}
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version, False)
profile = self.data.profile.get_profile_detail(
data["userId"], self.version, False
)
if profile is None:
return
@ -375,7 +381,9 @@ class Mai2Base:
return {"userId": data["userId"], "userExtend": extend_dict}
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
options = self.data.profile.get_profile_option(data["userId"], self.version, False)
options = self.data.profile.get_profile_option(
data["userId"], self.version, False
)
if options is None:
return
@ -446,23 +454,27 @@ class Mai2Base:
}
def handle_get_user_present_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []}
return {"userId": data.get("userId", 0), "length": 0, "userPresentList": []}
def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict:
return {}
def handle_get_user_present_event_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []}
return {
"userId": data.get("userId", 0),
"length": 0,
"userPresentEventList": [],
}
def handle_get_user_boss_api_request(self, data: Dict) -> Dict:
b = self.data.profile.get_boss_list(data["userId"])
if b is None:
return { "userId": data.get("userId", 0), "userBossData": {}}
return {"userId": data.get("userId", 0), "userBossData": {}}
boss_lst = b._asdict()
boss_lst.pop("id")
boss_lst.pop("user")
return { "userId": data.get("userId", 0), "userBossData": boss_lst}
return {"userId": data.get("userId", 0), "userBossData": boss_lst}
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000)
@ -539,11 +551,15 @@ class Mai2Base:
rating = self.data.profile.get_recent_rating(data["userId"])
if rating is None:
return
r = rating._asdict()
lst = r.get("userRecentRatingList", [])
return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst}
return {
"userId": data["userId"],
"length": len(lst),
"userRecentRatingList": lst,
}
def handle_get_user_rating_api_request(self, data: Dict) -> Dict:
rating = self.data.profile.get_profile_rating(data["userId"], self.version)
@ -707,13 +723,13 @@ class Mai2Base:
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_web_option_api_request(self, data: Dict) -> Dict:
w = self.data.profile.get_web_option(data["userId"], self.version)
if w is None:
if w is None:
return {"userId": data["userId"], "userWebOption": {}}
web_opt = w._asdict()
web_opt = w._asdict()
web_opt.pop("id")
web_opt.pop("user")
web_opt.pop("version")
@ -726,32 +742,46 @@ class Mai2Base:
def handle_get_user_grade_api_request(self, data: Dict) -> Dict:
g = self.data.profile.get_grade_status(data["userId"])
if g is None:
return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []}
return {
"userId": data["userId"],
"userGradeStatus": {},
"length": 0,
"userGradeList": [],
}
grade_stat = g._asdict()
grade_stat.pop("id")
grade_stat.pop("user")
return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []}
return {
"userId": data["userId"],
"userGradeStatus": grade_stat,
"length": 0,
"userGradeList": [],
}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 50)
upper_lim = next_index + max_ct
music_detail_list = []
if user_id <= 0:
self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
self.logger.warn(
"handle_get_user_music_api_request: Could not find userid in data, or userId is 0"
)
return {}
songs = self.data.score.get_best_scores(user_id, is_dx=False)
if songs is None:
self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!")
self.logger.debug(
"handle_get_user_music_api_request: get_best_scores returned None!"
)
return {
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
num_user_songs = len(songs)
@ -764,8 +794,14 @@ class Mai2Base:
tmp.pop("user")
music_detail_list.append(tmp)
next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
next_index = (
0
if len(music_detail_list) < max_ct or num_user_songs == upper_lim
else upper_lim
)
self.logger.info(
f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})"
)
return {
"userId": data["userId"],
"nextIndex": next_index,
@ -776,14 +812,17 @@ class Mai2Base:
self.logger.debug(data)
def handle_upload_user_photo_api_request(self, data: Dict) -> Dict:
if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir:
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if (
not self.game_config.uploads.photos
or not self.game_config.uploads.photos_dir
):
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
photo = data.get("userPhoto", {})
if photo is None or not photo:
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
order_id = int(photo.get("orderId", -1))
user_id = int(photo.get("userId", -1))
div_num = int(photo.get("divNumber", -1))
@ -793,72 +832,95 @@ class Mai2Base:
track_num = int(photo.get("trackNo", -1))
upload_date = photo.get("uploadDate", "")
if order_id < 0 or user_id <= 0 or div_num < 0 or div_len <= 0 or not div_data or playlog_id < 0 or track_num <= 0 or not upload_date:
if (
order_id < 0
or user_id <= 0
or div_num < 0
or div_len <= 0
or not div_data
or playlog_id < 0
or track_num <= 0
or not upload_date
):
self.logger.warn(f"Malformed photo upload request")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if order_id == 0 and div_num > 0:
self.logger.warn(f"Failed to set orderId properly (still 0 after first chunk)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
self.logger.warn(
f"Failed to set orderId properly (still 0 after first chunk)"
)
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if div_num == 0 and order_id > 0:
self.logger.warn(f"First chuck re-send, Ignore")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if div_num >= div_len:
self.logger.warn(f"Sent extra chunks ({div_num} >= {div_len})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if div_len >= 100:
self.logger.warn(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
self.logger.warn(
f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)"
)
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
ret_code = order_id + 1
photo_chunk = b64decode(div_data)
if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len):
self.logger.warn(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}"
if len(photo_chunk) > 10240 or (
len(photo_chunk) < 10240 and div_num + 1 != div_len
):
self.logger.warn(
f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})"
)
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
out_name = (
f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}"
)
if not path.exists(f"{out_name}.bin") and div_num != 0:
self.logger.warn(f"Out of order photo upload (div_num {div_num})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if path.exists(f"{out_name}.bin") and div_num == 0:
self.logger.warn(f"Duplicate file upload")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
elif path.exists(f"{out_name}.bin"):
fstats = stat(f"{out_name}.bin")
if fstats.st_size != 10240 * div_num:
self.logger.warn(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
self.logger.warn(
f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)"
)
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
try:
with open(f"{out_name}.bin", "ab") as f:
f.write(photo_chunk)
except Exception:
self.logger.error(f"Failed writing to {out_name}.bin")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
return {"returnCode": 0, "apiName": "UploadUserPhotoApi"}
if div_num + 1 == div_len and path.exists(f"{out_name}.bin"):
try:
p = ImageFile.Parser()
with open(f"{out_name}.bin", "rb") as f:
p.feed(f.read())
im = p.close()
im.save(f"{out_name}.jpeg")
except Exception:
self.logger.error(f"File {out_name}.bin failed image validation")
try:
remove(f"{out_name}.bin")
except Exception:
self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually")
return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'}
except Exception:
self.logger.error(
f"Failed to delete {out_name}.bin, please remove it manually"
)
return {"returnCode": ret_code, "apiName": "UploadUserPhotoApi"}

View File

@ -19,6 +19,7 @@ class Mai2ServerConfig:
)
)
class Mai2DeliverConfig:
def __init__(self, parent: "Mai2Config") -> None:
self.__config = parent
@ -34,17 +35,18 @@ class Mai2DeliverConfig:
return CoreConfig.get_config_field(
self.__config, "mai2", "deliver", "udbdl_enable", default=False
)
@property
def content_folder(self) -> int:
return CoreConfig.get_config_field(
self.__config, "mai2", "server", "content_folder", default=""
)
class Mai2UploadsConfig:
def __init__(self, parent: "Mai2Config") -> None:
self.__config = parent
@property
def photos(self) -> bool:
return CoreConfig.get_config_field(

View File

@ -20,13 +20,13 @@ class Mai2Constants:
DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
GAME_CODE = "SBXL"
GAME_CODE_GREEN = "SBZF"
GAME_CODE_ORANGE = "SDBM"
GAME_CODE_PINK = "SDCQ"
GAME_CODE_MURASAKI = "SDDK"
GAME_CODE_MILK = "SDDZ"
GAME_CODE_FINALE = "SDEY"
GAME_CODE = "SBXL"
GAME_CODE_GREEN = "SBZF"
GAME_CODE_ORANGE = "SDBM"
GAME_CODE_PINK = "SDCQ"
GAME_CODE_MURASAKI = "SDDK"
GAME_CODE_MILK = "SDDZ"
GAME_CODE_FINALE = "SDEY"
GAME_CODE_DX = "SDEZ"
CONFIG_NAME = "mai2.yaml"
@ -66,7 +66,7 @@ class Mai2Constants:
"maimai MURASAKi PLUS",
"maimai MiLK",
"maimai MiLK PLUS",
"maimai FiNALE",
"maimai FiNALE",
"maimai DX",
"maimai DX PLUS",
"maimai DX Splash",

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
import pytz
import json
from random import randint
from contextlib import nullcontext
from core.config import CoreConfig
from titles.mai2.base import Mai2Base
@ -14,13 +15,13 @@ class Mai2DX(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEZ/100/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/"
def handle_get_game_setting_api_request(self, data: Dict):
return {
"gameSetting": {
@ -31,9 +32,13 @@ class Mai2DX(Mai2Base):
"movieUploadLimit": 100,
"movieStatus": 1,
"movieServerUri": self.old_server + "movie/",
"deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "",
"deliverServerUri": self.old_server + "deliver/"
if self.can_deliver and self.game_config.deliver.enable
else "",
"oldServerUri": self.old_server + "old",
"usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "",
"usbDlServerUri": self.old_server + "usbdl/"
if self.can_deliver and self.game_config.deliver.udbdl_enable
else "",
"rebootInterval": 0,
},
"isAouAccession": False,
@ -103,117 +108,119 @@ class Mai2DX(Mai2Base):
user_id = data["userId"]
upsert = data["upsertUserAll"]
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0]["isNetMember"] = 1
upsert["userData"][0].pop("accessCode")
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0]
)
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_ghost(user_id, self.version, ghost)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0]
)
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for k, v in upsert["userActivityList"][0].items():
for act in v:
self.data.profile.put_profile_activity(user_id, act)
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
for charge in upsert["userChargeList"]:
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
),
datetime.strptime(
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
),
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0]["isNetMember"] = 1
upsert["userData"][0].pop("accessCode")
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0]
)
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character(
user_id,
char["characterId"],
char["level"],
char["awakening"],
char["useCount"],
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item["stock"],
item["isValid"],
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_ghost(user_id, self.version, ghost)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0]
)
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for k, v in upsert["userActivityList"][0].items():
for act in v:
self.data.profile.put_profile_activity(user_id, act)
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music)
with self.data.item.conn.begin() or nullcontext():
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
for charge in upsert["userChargeList"]:
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
),
datetime.strptime(
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
),
)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character(
user_id,
char["characterId"],
char["level"],
char["awakening"],
char["useCount"],
)
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item["stock"],
item["isValid"],
)
if (
"userFriendSeasonRankingList" in upsert
and len(upsert["userFriendSeasonRankingList"]) > 0
):
for fsr in upsert["userFriendSeasonRankingList"]:
fsr["recordDate"] = (
datetime.strptime(
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
),
)
self.data.item.put_friend_season_ranking(user_id, fsr)
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
)
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
)
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
if (
"userFriendSeasonRankingList" in upsert
and len(upsert["userFriendSeasonRankingList"]) > 0
):
for fsr in upsert["userFriendSeasonRankingList"]:
fsr["recordDate"] = (
datetime.strptime(
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
),
)
self.data.item.put_friend_season_ranking(user_id, fsr)
with self.data.score.conn.begin() or nullcontext():
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
return {"returnCode": 1, "apiName": "UpsertUserAllApi"}
@ -545,24 +552,28 @@ class Mai2DX(Mai2Base):
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 50)
upper_lim = next_index + max_ct
music_detail_list = []
if user_id <= 0:
self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
self.logger.warn(
"handle_get_user_music_api_request: Could not find userid in data, or userId is 0"
)
return {}
songs = self.data.score.get_best_scores(user_id)
if songs is None:
self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!")
self.logger.debug(
"handle_get_user_music_api_request: get_best_scores returned None!"
)
return {
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
num_user_songs = len(songs)
@ -575,8 +586,14 @@ class Mai2DX(Mai2Base):
tmp.pop("user")
music_detail_list.append(tmp)
next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
next_index = (
0
if len(music_detail_list) < max_ct or num_user_songs == upper_lim
else upper_lim
)
self.logger.info(
f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})"
)
return {
"userId": data["userId"],
"nextIndex": next_index,
@ -587,5 +604,5 @@ class Mai2DX(Mai2Base):
ret = super().handle_user_login_api_request(data)
if ret is None or not ret:
return ret
ret['loginId'] = ret.get('loginCount', 0)
ret["loginId"] = ret.get("loginCount", 0)
return ret

View File

@ -15,9 +15,9 @@ class Mai2Finale(Mai2Base):
self.version = Mai2Constants.VER_MAIMAI_FINALE
self.can_deliver = True
self.can_usbdl = True
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/"

View File

@ -47,7 +47,7 @@ class Mai2Servlet:
None,
None,
None,
Mai2Finale,
Mai2Finale,
Mai2DX,
Mai2DXPlus,
Mai2Splash,
@ -110,22 +110,34 @@ class Mai2Servlet:
)
def setup(self):
if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir):
if (
self.game_cfg.uploads.photos
and self.game_cfg.uploads.photos_dir
and not path.exists(self.game_cfg.uploads.photos_dir)
):
try:
mkdir(self.game_cfg.uploads.photos_dir)
except Exception:
self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}")
self.logger.error(
f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}"
)
if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir):
if (
self.game_cfg.uploads.movies
and self.game_cfg.uploads.movies_dir
and not path.exists(self.game_cfg.uploads.movies_dir)
):
try:
mkdir(self.game_cfg.uploads.movies_dir)
except Exception:
self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}")
self.logger.error(
f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}"
)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.lower() == "ping":
return zlib.compress(b'{"returnCode": "1"}')
elif url_path.startswith("api/movie/"):
self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}")
return b""
@ -180,7 +192,7 @@ class Mai2Servlet:
internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS
elif version >= 197: # Finale
internal_ver = Mai2Constants.VER_MAIMAI_FINALE
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
@ -228,9 +240,11 @@ class Mai2Servlet:
self.logger.info(f"v{version} GET {url_path}")
url_split = url_path.split("/")
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie":
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[
0
] == "movie":
if url_split[2] == "moviestart":
return json.dumps({"moviestart":{"status":"OK"}}).encode()
return json.dumps({"moviestart": {"status": "OK"}}).encode()
if url_split[0] == "old":
if url_split[1] == "ping":
@ -244,7 +258,7 @@ class Mai2Servlet:
elif url_split[1].startswith("friend"):
self.logger.info(f"v{version} old server friend inquire")
return zlib.compress(b"{}")
elif url_split[0] == "usbdl":
if url_split[1] == "CONNECTIONTEST":
self.logger.info(f"v{version} usbdl server test")
@ -253,9 +267,11 @@ class Mai2Servlet:
elif url_split[0] == "deliver":
file = url_split[len(url_split) - 1]
self.logger.info(f"v{version} {file} deliver inquire")
if not self.game_cfg.deliver.enable or not path.exists(f"{self.game_cfg.deliver.content_folder}/{file}"):
if not self.game_cfg.deliver.enable or not path.exists(
f"{self.game_cfg.deliver.content_folder}/{file}"
):
return zlib.compress(b"")
else:
return zlib.compress(b"{}")

View File

@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional
from Crypto.Cipher import AES
import zlib
import codecs
from contextlib import nullcontext
from core.config import CoreConfig
from core.data import Data
@ -46,64 +47,84 @@ class Mai2Reader(BaseReader):
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
self.get_events(f"{dir}/event")
self.disable_events(f"{dir}/information", f"{dir}/scoreRanking")
self.read_music(f"{dir}/music")
self.read_tickets(f"{dir}/ticket")
with self.data.static.conn.begin() or nullcontext():
self.get_events(f"{dir}/event")
self.disable_events(f"{dir}/information", f"{dir}/scoreRanking")
self.read_music(f"{dir}/music")
self.read_tickets(f"{dir}/ticket")
else:
if not os.path.exists(f"{self.bin_dir}/tables"):
self.logger.error(f"tables directory not found in {self.bin_dir}")
return
if self.version >= Mai2Constants.VER_MAIMAI_MILK:
if self.extra is None:
self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag")
self.logger.error(
"Milk - Finale requre an AES key via a hex string send as the --extra flag"
)
return
key = bytes.fromhex(self.extra)
else:
key = None
evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key)
txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key)
score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key)
evt_table = self.load_table_raw(
f"{self.bin_dir}/tables", "mmEvent.bin", key
)
txt_table = self.load_table_raw(
f"{self.bin_dir}/tables", "mmtextout_jp.bin", key
)
score_table = self.load_table_raw(
f"{self.bin_dir}/tables", "mmScore.bin", key
)
self.read_old_events(evt_table)
self.read_old_music(score_table, txt_table)
if self.opt_dir is not None:
evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key)
txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key)
score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key)
evt_table = self.load_table_raw(
f"{self.opt_dir}/tables", "mmEvent.bin", key
)
txt_table = self.load_table_raw(
f"{self.opt_dir}/tables", "mmtextout_jp.bin", key
)
score_table = self.load_table_raw(
f"{self.opt_dir}/tables", "mmScore.bin", key
)
self.read_old_events(evt_table)
self.read_old_music(score_table, txt_table)
return
def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]:
def load_table_raw(
self, dir: str, file: str, key: Optional[bytes]
) -> Optional[List[Dict[str, str]]]:
if not os.path.exists(f"{dir}/{file}"):
self.logger.warn(f"file {file} does not exist in directory {dir}, skipping")
return
self.logger.info(f"Load table {file} from {dir}")
if key is not None:
cipher = AES.new(key, AES.MODE_CBC)
with open(f"{dir}/{file}", "rb") as f:
f_encrypted = f.read()
f_data = cipher.decrypt(f_encrypted)[0x10:]
else:
with open(f"{dir}/{file}", "rb") as f:
f_data = f.read()[0x10:]
if f_data is None or not f_data:
self.logger.warn(f"file {dir} could not be read, skipping")
return
f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning
f_data_deflate = zlib.decompress(f_data, wbits=zlib.MAX_WBITS | 16)[
0x12:
] # lop off the junk at the beginning
f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0]
f_split = f_decoded.splitlines()
@ -118,31 +139,31 @@ class Mai2Reader(BaseReader):
is_struct = True
struct_name = x[7:-1]
continue
if x.startswith("};"):
is_struct = False
break
if is_struct:
try:
struct_def.append(x[x.rindex(" ") + 2: -1])
struct_def.append(x[x.rindex(" ") + 2 : -1])
except ValueError:
self.logger.warn(f"rindex failed on line {x}")
if is_struct:
self.logger.warn("Struct not formatted properly")
if not struct_def:
self.logger.warn("Struct def not found")
name = file[:file.index(".")]
name = file[: file.index(".")]
if "_" in name:
name = name[:file.index("_")]
name = name[: file.index("_")]
for x in f_split:
if not x.startswith(name.upper()):
continue
line_match = re.match(r"(\w+)\((.*?)\)([ ]+\/{3}<[ ]+(.*))?", x)
if line_match is None:
continue
@ -150,31 +171,31 @@ class Mai2Reader(BaseReader):
if not line_match.group(1) == name.upper():
self.logger.warn(f"Strange regex match for line {x} -> {line_match}")
continue
vals = line_match.group(2)
comment = line_match.group(4)
line_dict = {}
vals_split = vals.split(",")
for y in range(len(vals_split)):
stripped = vals_split[y].strip().lstrip("L\"").lstrip("\"").rstrip("\"")
stripped = vals_split[y].strip().lstrip('L"').lstrip('"').rstrip('"')
if not stripped or stripped is None:
continue
if has_struct_def and len(struct_def) > y:
line_dict[struct_def[y]] = stripped
else:
line_dict[f'item_{y}'] = stripped
line_dict[f"item_{y}"] = stripped
if comment:
line_dict['comment'] = comment
line_dict["comment"] = comment
tbl_content.append(line_dict)
if tbl_content:
return tbl_content
else:
self.logger.warning("Failed load table content, skipping")
return
@ -324,20 +345,24 @@ class Mai2Reader(BaseReader):
def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None:
if events is None:
return
for event in events:
evt_id = int(event.get('イベントID', '0'))
evt_expire_time = float(event.get('オフ時強制時期', '0.0'))
is_exp = bool(int(event.get('海外許可', '0')))
is_aou = bool(int(event.get('AOU許可', '0')))
name = event.get('comment', f'evt_{evt_id}')
evt_id = int(event.get("イベントID", "0"))
evt_expire_time = float(event.get("オフ時強制時期", "0.0"))
is_exp = bool(int(event.get("海外許可", "0")))
is_aou = bool(int(event.get("AOU許可", "0")))
name = event.get("comment", f"evt_{evt_id}")
self.data.static.put_game_event(self.version, 0, evt_id, name)
if not (is_exp or is_aou):
self.data.static.toggle_game_event(self.version, evt_id, False)
def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None:
def read_old_music(
self,
scores: Optional[List[Dict[str, str]]],
text: Optional[List[Dict[str, str]]],
) -> None:
if scores is None or text is None:
return
# TODO

View File

@ -1,6 +1,6 @@
from titles.mai2.schema.profile import Mai2ProfileData
from titles.mai2.schema.item import Mai2ItemData
from titles.mai2.schema.static import Mai2StaticData
from titles.mai2.schema.score import Mai2ScoreData
__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData]
from titles.mai2.schema.profile import Mai2ProfileData
from titles.mai2.schema.item import Mai2ItemData
from titles.mai2.schema.static import Mai2StaticData
from titles.mai2.schema.score import Mai2ScoreData
__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,365 +1,376 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from core.data import cached
best_score = Table(
"mai2_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("achievement", Integer),
Column("comboStatus", Integer),
Column("syncStatus", Integer),
Column("deluxscoreMax", Integer),
Column("scoreRank", Integer),
Column("extNum1", Integer, server_default="0"),
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"mai2_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userId", BigInteger),
Column("orderId", Integer),
Column("playlogId", BigInteger),
Column("version", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("loginDate", BigInteger),
Column("playDate", String(255)),
Column("userPlayDate", String(255)),
Column("type", Integer),
Column("musicId", Integer),
Column("level", Integer),
Column("trackNo", Integer),
Column("vsMode", Integer),
Column("vsUserName", String(255)),
Column("vsStatus", Integer),
Column("vsUserRating", Integer),
Column("vsUserAchievement", Integer),
Column("vsUserGradeRank", Integer),
Column("vsRank", Integer),
Column("playerNum", Integer),
Column("playedUserId1", BigInteger),
Column("playedUserName1", String(255)),
Column("playedMusicLevel1", Integer),
Column("playedUserId2", BigInteger),
Column("playedUserName2", String(255)),
Column("playedMusicLevel2", Integer),
Column("playedUserId3", BigInteger),
Column("playedUserName3", String(255)),
Column("playedMusicLevel3", Integer),
Column("characterId1", Integer),
Column("characterLevel1", Integer),
Column("characterAwakening1", Integer),
Column("characterId2", Integer),
Column("characterLevel2", Integer),
Column("characterAwakening2", Integer),
Column("characterId3", Integer),
Column("characterLevel3", Integer),
Column("characterAwakening3", Integer),
Column("characterId4", Integer),
Column("characterLevel4", Integer),
Column("characterAwakening4", Integer),
Column("characterId5", Integer),
Column("characterLevel5", Integer),
Column("characterAwakening5", Integer),
Column("achievement", Integer),
Column("deluxscore", Integer),
Column("scoreRank", Integer),
Column("maxCombo", Integer),
Column("totalCombo", Integer),
Column("maxSync", Integer),
Column("totalSync", Integer),
Column("tapCriticalPerfect", Integer),
Column("tapPerfect", Integer),
Column("tapGreat", Integer),
Column("tapGood", Integer),
Column("tapMiss", Integer),
Column("holdCriticalPerfect", Integer),
Column("holdPerfect", Integer),
Column("holdGreat", Integer),
Column("holdGood", Integer),
Column("holdMiss", Integer),
Column("slideCriticalPerfect", Integer),
Column("slidePerfect", Integer),
Column("slideGreat", Integer),
Column("slideGood", Integer),
Column("slideMiss", Integer),
Column("touchCriticalPerfect", Integer),
Column("touchPerfect", Integer),
Column("touchGreat", Integer),
Column("touchGood", Integer),
Column("touchMiss", Integer),
Column("breakCriticalPerfect", Integer),
Column("breakPerfect", Integer),
Column("breakGreat", Integer),
Column("breakGood", Integer),
Column("breakMiss", Integer),
Column("isTap", Boolean),
Column("isHold", Boolean),
Column("isSlide", Boolean),
Column("isTouch", Boolean),
Column("isBreak", Boolean),
Column("isCriticalDisp", Boolean),
Column("isFastLateDisp", Boolean),
Column("fastCount", Integer),
Column("lateCount", Integer),
Column("isAchieveNewRecord", Boolean),
Column("isDeluxscoreNewRecord", Boolean),
Column("comboStatus", Integer),
Column("syncStatus", Integer),
Column("isClear", Boolean),
Column("beforeRating", Integer),
Column("afterRating", Integer),
Column("beforeGrade", Integer),
Column("afterGrade", Integer),
Column("afterGradeRank", Integer),
Column("beforeDeluxRating", Integer),
Column("afterDeluxRating", Integer),
Column("isPlayTutorial", Boolean),
Column("isEventMode", Boolean),
Column("isFreedomMode", Boolean),
Column("playMode", Integer),
Column("isNewFree", Boolean),
Column("extNum1", Integer),
Column("extNum2", Integer),
Column("extNum4", Integer, server_default="0"),
Column("trialPlayAchievement", Integer),
mysql_charset="utf8mb4",
)
course = Table(
"mai2_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("isLastClear", Boolean),
Column("totalRestlife", Integer),
Column("totalAchievement", Integer),
Column("totalDeluxscore", Integer),
Column("playCount", Integer),
Column("clearDate", String(25)),
Column("lastPlayDate", String(25)),
Column("bestAchievement", Integer),
Column("bestAchievementDate", String(25)),
Column("bestDeluxscore", Integer),
Column("bestDeluxscoreDate", String(25)),
UniqueConstraint("user", "courseId", name="mai2_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog_old = Table(
"maimai_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer),
# Pop access code
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("country", String(255)),
Column("regionId", Integer),
Column("playDate", String(255)),
Column("userPlayDate", String(255)),
Column("musicId", Integer),
Column("level", Integer),
Column("gameMode", Integer),
Column("rivalNum", Integer),
Column("track", Integer),
Column("eventId", Integer),
Column("isFreeToPlay", Boolean),
Column("playerRating", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(255)),
Column("playedUserName2", String(255)),
Column("playedUserName3", String(255)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("achievement", Integer),
Column("score", Integer),
Column("tapScore", Integer),
Column("holdScore", Integer),
Column("slideScore", Integer),
Column("breakScore", Integer),
Column("syncRate", Integer),
Column("vsWin", Integer),
Column("isAllPerfect", Boolean),
Column("fullCombo", Integer),
Column("maxFever", Integer),
Column("maxCombo", Integer),
Column("tapPerfect", Integer),
Column("tapGreat", Integer),
Column("tapGood", Integer),
Column("tapBad", Integer),
Column("holdPerfect", Integer),
Column("holdGreat", Integer),
Column("holdGood", Integer),
Column("holdBad", Integer),
Column("slidePerfect", Integer),
Column("slideGreat", Integer),
Column("slideGood", Integer),
Column("slideBad", Integer),
Column("breakPerfect", Integer),
Column("breakGreat", Integer),
Column("breakGood", Integer),
Column("breakBad", Integer),
Column("judgeStyle", Integer),
Column("isTrackSkip", Boolean),
Column("isHighScore", Boolean),
Column("isChallengeTrack", Boolean),
Column("challengeLife", Integer),
Column("challengeRemain", Integer),
Column("isAllPerfectPlus", Integer),
mysql_charset="utf8mb4",
)
best_score_old = Table(
"maimai_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("achievement", Integer),
Column("scoreMax", Integer),
Column("syncRateMax", Integer),
Column("isAllPerfect", Boolean),
Column("isAllPerfectPlus", Integer),
Column("fullCombo", Integer),
Column("maxFever", Integer),
UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"),
mysql_charset="utf8mb4",
)
class Mai2ScoreData(BaseData):
def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]:
score_data["user"] = user_id
if is_dx:
sql = insert(best_score).values(**score_data)
else:
sql = insert(best_score_old).values(**score_data)
conflict = sql.on_duplicate_key_update(**score_data)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
@cached(2)
def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]:
if is_dx:
sql = best_score.select(
and_(
best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True,
)
)
else:
sql = best_score_old.select(
and_(
best_score_old.c.user == user_id,
(best_score_old.c.song_id == song_id) if song_id is not None else True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_score(
self, user_id: int, song_id: int, chart_id: int
) -> Optional[Row]:
sql = best_score.select(
and_(
best_score.c.user == user_id,
best_score.c.song_id == song_id,
best_score.c.chart_id == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]:
playlog_data["user"] = user_id
if is_dx:
sql = insert(playlog).values(**playlog_data)
else:
sql = insert(playlog_old).values(**playlog_data)
conflict = sql.on_duplicate_key_update(**playlog_data)
result = self.execute(conflict)
if result is None:
self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}")
return None
return result.lastrowid
def put_course(self, user_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = user_id
sql = insert(course).values(**course_data)
conflict = sql.on_duplicate_key_update(**course_data)
result = self.execute(conflict)
if result is None:
self.logger.error(f"put_course: Failed to insert! user_id {user_id}")
return None
return result.lastrowid
def get_courses(self, user_id: int) -> Optional[List[Row]]:
sql = course.select(course.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
from core.data import cached
best_score = Table(
"mai2_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("achievement", Integer),
Column("comboStatus", Integer),
Column("syncStatus", Integer),
Column("deluxscoreMax", Integer),
Column("scoreRank", Integer),
Column("extNum1", Integer, server_default="0"),
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"mai2_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userId", BigInteger),
Column("orderId", Integer),
Column("playlogId", BigInteger),
Column("version", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("loginDate", BigInteger),
Column("playDate", String(255)),
Column("userPlayDate", String(255)),
Column("type", Integer),
Column("musicId", Integer),
Column("level", Integer),
Column("trackNo", Integer),
Column("vsMode", Integer),
Column("vsUserName", String(255)),
Column("vsStatus", Integer),
Column("vsUserRating", Integer),
Column("vsUserAchievement", Integer),
Column("vsUserGradeRank", Integer),
Column("vsRank", Integer),
Column("playerNum", Integer),
Column("playedUserId1", BigInteger),
Column("playedUserName1", String(255)),
Column("playedMusicLevel1", Integer),
Column("playedUserId2", BigInteger),
Column("playedUserName2", String(255)),
Column("playedMusicLevel2", Integer),
Column("playedUserId3", BigInteger),
Column("playedUserName3", String(255)),
Column("playedMusicLevel3", Integer),
Column("characterId1", Integer),
Column("characterLevel1", Integer),
Column("characterAwakening1", Integer),
Column("characterId2", Integer),
Column("characterLevel2", Integer),
Column("characterAwakening2", Integer),
Column("characterId3", Integer),
Column("characterLevel3", Integer),
Column("characterAwakening3", Integer),
Column("characterId4", Integer),
Column("characterLevel4", Integer),
Column("characterAwakening4", Integer),
Column("characterId5", Integer),
Column("characterLevel5", Integer),
Column("characterAwakening5", Integer),
Column("achievement", Integer),
Column("deluxscore", Integer),
Column("scoreRank", Integer),
Column("maxCombo", Integer),
Column("totalCombo", Integer),
Column("maxSync", Integer),
Column("totalSync", Integer),
Column("tapCriticalPerfect", Integer),
Column("tapPerfect", Integer),
Column("tapGreat", Integer),
Column("tapGood", Integer),
Column("tapMiss", Integer),
Column("holdCriticalPerfect", Integer),
Column("holdPerfect", Integer),
Column("holdGreat", Integer),
Column("holdGood", Integer),
Column("holdMiss", Integer),
Column("slideCriticalPerfect", Integer),
Column("slidePerfect", Integer),
Column("slideGreat", Integer),
Column("slideGood", Integer),
Column("slideMiss", Integer),
Column("touchCriticalPerfect", Integer),
Column("touchPerfect", Integer),
Column("touchGreat", Integer),
Column("touchGood", Integer),
Column("touchMiss", Integer),
Column("breakCriticalPerfect", Integer),
Column("breakPerfect", Integer),
Column("breakGreat", Integer),
Column("breakGood", Integer),
Column("breakMiss", Integer),
Column("isTap", Boolean),
Column("isHold", Boolean),
Column("isSlide", Boolean),
Column("isTouch", Boolean),
Column("isBreak", Boolean),
Column("isCriticalDisp", Boolean),
Column("isFastLateDisp", Boolean),
Column("fastCount", Integer),
Column("lateCount", Integer),
Column("isAchieveNewRecord", Boolean),
Column("isDeluxscoreNewRecord", Boolean),
Column("comboStatus", Integer),
Column("syncStatus", Integer),
Column("isClear", Boolean),
Column("beforeRating", Integer),
Column("afterRating", Integer),
Column("beforeGrade", Integer),
Column("afterGrade", Integer),
Column("afterGradeRank", Integer),
Column("beforeDeluxRating", Integer),
Column("afterDeluxRating", Integer),
Column("isPlayTutorial", Boolean),
Column("isEventMode", Boolean),
Column("isFreedomMode", Boolean),
Column("playMode", Integer),
Column("isNewFree", Boolean),
Column("extNum1", Integer),
Column("extNum2", Integer),
Column("extNum4", Integer, server_default="0"),
Column("trialPlayAchievement", Integer),
mysql_charset="utf8mb4",
)
course = Table(
"mai2_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("isLastClear", Boolean),
Column("totalRestlife", Integer),
Column("totalAchievement", Integer),
Column("totalDeluxscore", Integer),
Column("playCount", Integer),
Column("clearDate", String(25)),
Column("lastPlayDate", String(25)),
Column("bestAchievement", Integer),
Column("bestAchievementDate", String(25)),
Column("bestDeluxscore", Integer),
Column("bestDeluxscoreDate", String(25)),
UniqueConstraint("user", "courseId", name="mai2_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog_old = Table(
"maimai_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer),
# Pop access code
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("country", String(255)),
Column("regionId", Integer),
Column("playDate", String(255)),
Column("userPlayDate", String(255)),
Column("musicId", Integer),
Column("level", Integer),
Column("gameMode", Integer),
Column("rivalNum", Integer),
Column("track", Integer),
Column("eventId", Integer),
Column("isFreeToPlay", Boolean),
Column("playerRating", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(255)),
Column("playedUserName2", String(255)),
Column("playedUserName3", String(255)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("achievement", Integer),
Column("score", Integer),
Column("tapScore", Integer),
Column("holdScore", Integer),
Column("slideScore", Integer),
Column("breakScore", Integer),
Column("syncRate", Integer),
Column("vsWin", Integer),
Column("isAllPerfect", Boolean),
Column("fullCombo", Integer),
Column("maxFever", Integer),
Column("maxCombo", Integer),
Column("tapPerfect", Integer),
Column("tapGreat", Integer),
Column("tapGood", Integer),
Column("tapBad", Integer),
Column("holdPerfect", Integer),
Column("holdGreat", Integer),
Column("holdGood", Integer),
Column("holdBad", Integer),
Column("slidePerfect", Integer),
Column("slideGreat", Integer),
Column("slideGood", Integer),
Column("slideBad", Integer),
Column("breakPerfect", Integer),
Column("breakGreat", Integer),
Column("breakGood", Integer),
Column("breakBad", Integer),
Column("judgeStyle", Integer),
Column("isTrackSkip", Boolean),
Column("isHighScore", Boolean),
Column("isChallengeTrack", Boolean),
Column("challengeLife", Integer),
Column("challengeRemain", Integer),
Column("isAllPerfectPlus", Integer),
mysql_charset="utf8mb4",
)
best_score_old = Table(
"maimai_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("achievement", Integer),
Column("scoreMax", Integer),
Column("syncRateMax", Integer),
Column("isAllPerfect", Boolean),
Column("isAllPerfectPlus", Integer),
Column("fullCombo", Integer),
Column("maxFever", Integer),
UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"),
mysql_charset="utf8mb4",
)
class Mai2ScoreData(BaseData):
def put_best_score(
self, user_id: int, score_data: Dict, is_dx: bool = True
) -> Optional[int]:
score_data["user"] = user_id
if is_dx:
sql = insert(best_score).values(**score_data)
else:
sql = insert(best_score_old).values(**score_data)
conflict = sql.on_conflict_do_update(set_=dict(**score_data))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
@cached(2)
def get_best_scores(
self, user_id: int, song_id: int = None, is_dx: bool = True
) -> Optional[List[Row]]:
if is_dx:
sql = best_score.select(
and_(
best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True,
)
)
else:
sql = best_score_old.select(
and_(
best_score_old.c.user == user_id,
(best_score_old.c.song_id == song_id)
if song_id is not None
else True,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_score(
self, user_id: int, song_id: int, chart_id: int
) -> Optional[Row]:
sql = best_score.select(
and_(
best_score.c.user == user_id,
best_score.c.song_id == song_id,
best_score.c.chart_id == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_playlog(
self, user_id: int, playlog_data: Dict, is_dx: bool = True
) -> Optional[int]:
playlog_data["user"] = user_id
if is_dx:
sql = insert(playlog).values(**playlog_data)
else:
sql = insert(playlog_old).values(**playlog_data)
conflict = sql.on_conflict_do_update(set_=dict(**playlog_data))
result = self.execute(conflict)
if result is None:
self.logger.error(
f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
def put_course(self, user_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = user_id
sql = insert(course).values(**course_data)
conflict = sql.on_conflict_do_update(set_=dict(**course_data))
result = self.execute(conflict)
if result is None:
self.logger.error(f"put_course: Failed to insert! user_id {user_id}")
return None
return result.lastrowid
def get_courses(self, user_id: int) -> Optional[List[Row]]:
sql = course.select(course.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -1,250 +1,252 @@
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
event = Table(
"mai2_static_event",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
mysql_charset="utf8mb4",
)
music = Table(
"mai2_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("genre", String(255)),
Column("bpm", Integer),
Column("addedVersion", String(255)),
Column("difficulty", Float),
Column("noteDesigner", String(255)),
UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"),
mysql_charset="utf8mb4",
)
ticket = Table(
"mai2_static_ticket",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("ticketId", Integer),
Column("kind", Integer),
Column("name", String(255)),
Column("price", Integer, server_default="1"),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"),
mysql_charset="utf8mb4",
)
cards = Table(
"mai2_static_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("cardName", String(255), nullable=False),
Column("startDate", TIMESTAMP, server_default="2018-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("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"),
mysql_charset="utf8mb4",
)
class Mai2StaticData(BaseData):
def put_game_event(
self, version: int, type: int, event_id: int, name: str
) -> Optional[int]:
sql = insert(event).values(
version=version,
type=type,
eventId=event_id,
name=name,
)
conflict = sql.on_duplicate_key_update(eventId=event_id)
result = self.execute(conflict)
if result is None:
self.logger.warning(
f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}"
)
return result.lastrowid
def get_game_events(self, version: int) -> Optional[List[Row]]:
sql = event.select(event.c.version == version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_enabled_events(self, version: int) -> Optional[List[Row]]:
sql = select(event).where(
and_(event.c.version == version, event.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def toggle_game_event(
self, version: int, event_id: int, toggle: bool
) -> Optional[List]:
sql = event.update(
and_(event.c.version == version, event.c.eventId == event_id)
).values(enabled=int(toggle))
result = self.execute(sql)
if result is None:
self.logger.warning(
f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}"
)
return result.last_updated_params()
def put_game_music(
self,
version: int,
song_id: int,
chart_id: int,
title: str,
artist: str,
genre: str,
bpm: str,
added_version: str,
difficulty: float,
note_designer: str,
) -> None:
sql = insert(music).values(
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
conflict = sql.on_duplicate_key_update(
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}")
return None
return result.lastrowid
def put_game_ticket(
self,
version: int,
ticket_id: int,
ticket_type: int,
ticket_price: int,
name: str,
) -> Optional[int]:
sql = insert(ticket).values(
version=version,
ticketId=ticket_id,
kind=ticket_type,
price=ticket_price,
name=name,
)
conflict = sql.on_duplicate_key_update(price=ticket_price)
conflict = sql.on_duplicate_key_update(price=ticket_price)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}")
return None
return result.lastrowid
def get_enabled_tickets(
self, version: int, kind: int = None
) -> Optional[List[Row]]:
if kind is not None:
sql = select(ticket).where(
and_(
ticket.c.version == version,
ticket.c.enabled == True,
ticket.c.kind == kind,
)
)
else:
sql = select(ticket).where(
and_(ticket.c.version == version, ticket.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int:
sql = insert(cards).values(
version=version, cardId=card_id, cardName=card_name, **card_data
)
conflict = sql.on_duplicate_key_update(**card_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card {card_id}")
return None
return result.lastrowid
def get_enabled_cards(self, version: int) -> Optional[List[Row]]:
sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
event = Table(
"mai2_static_event",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
mysql_charset="utf8mb4",
)
music = Table(
"mai2_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("genre", String(255)),
Column("bpm", Integer),
Column("addedVersion", String(255)),
Column("difficulty", Float),
Column("noteDesigner", String(255)),
UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"),
mysql_charset="utf8mb4",
)
ticket = Table(
"mai2_static_ticket",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("ticketId", Integer),
Column("kind", Integer),
Column("name", String(255)),
Column("price", Integer, server_default="1"),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"),
mysql_charset="utf8mb4",
)
cards = Table(
"mai2_static_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("cardName", String(255), nullable=False),
Column("startDate", TIMESTAMP, server_default="2018-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("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"),
mysql_charset="utf8mb4",
)
class Mai2StaticData(BaseData):
def put_game_event(
self, version: int, type: int, event_id: int, name: str
) -> Optional[int]:
sql = insert(event).values(
version=version,
type=type,
eventId=event_id,
name=name,
)
conflict = sql.on_conflict_do_update(set_=dict(eventId=event_id))
result = self.execute(conflict)
if result is None:
self.logger.warning(
f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}"
)
return result.lastrowid
def get_game_events(self, version: int) -> Optional[List[Row]]:
sql = event.select(event.c.version == version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_enabled_events(self, version: int) -> Optional[List[Row]]:
sql = select(event).where(
and_(event.c.version == version, event.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def toggle_game_event(
self, version: int, event_id: int, toggle: bool
) -> Optional[List]:
sql = event.update(
and_(event.c.version == version, event.c.eventId == event_id)
).values(enabled=int(toggle))
result = self.execute(sql)
if result is None:
self.logger.warning(
f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}"
)
return result.last_updated_params()
def put_game_music(
self,
version: int,
song_id: int,
chart_id: int,
title: str,
artist: str,
genre: str,
bpm: str,
added_version: str,
difficulty: float,
note_designer: str,
) -> None:
sql = insert(music).values(
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
conflict = sql.on_conflict_do_update(
set_=dict(
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}")
return None
return result.lastrowid
def put_game_ticket(
self,
version: int,
ticket_id: int,
ticket_type: int,
ticket_price: int,
name: str,
) -> Optional[int]:
sql = insert(ticket).values(
version=version,
ticketId=ticket_id,
kind=ticket_type,
price=ticket_price,
name=name,
)
conflict = sql.on_conflict_do_update(set_=dict(price=ticket_price))
conflict = sql.on_conflict_do_update(set_=dict(price=ticket_price))
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}")
return None
return result.lastrowid
def get_enabled_tickets(
self, version: int, kind: int = None
) -> Optional[List[Row]]:
if kind is not None:
sql = select(ticket).where(
and_(
ticket.c.version == version,
ticket.c.enabled == True,
ticket.c.kind == kind,
)
)
else:
sql = select(ticket).where(
and_(ticket.c.version == version, ticket.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int:
sql = insert(cards).values(
version=version, cardId=card_id, cardName=card_name, **card_data
)
conflict = sql.on_conflict_do_update(set_=dict(**card_data))
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card {card_id}")
return None
return result.lastrowid
def get_enabled_cards(self, version: int) -> Optional[List[Row]]:
sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -3,6 +3,7 @@ from random import randint
from datetime import datetime, timedelta
import pytz
import json
from contextlib import nullcontext
from core.config import CoreConfig
from titles.mai2.splashplus import Mai2SplashPlus
@ -70,8 +71,12 @@ class Mai2Universe(Mai2SplashPlus):
tmp.pop("cardName")
tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["endDate"] = datetime.strftime(tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["startDate"] = datetime.strftime(
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
)
@ -163,38 +168,43 @@ class Mai2Universe(Mai2SplashPlus):
end_date = datetime.utcnow() + timedelta(days=15)
user_card = upsert["userCard"]
self.data.item.put_card(
user_id,
user_card["cardId"],
user_card["cardTypeId"],
user_card["charaId"],
user_card["mapId"],
# add the correct start date and also the end date in 15 days
start_date,
end_date,
)
# get the profile extend to save the new bought card
extend = self.data.profile.get_profile_extend(user_id, self.version)
if extend:
extend = extend._asdict()
# parse the selectedCardList
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
selected_cards: List = extend["selectedCardList"]
with (
self.data.item.conn.begin() or nullcontext(),
self.data.profile.conn.begin() or nullcontext(),
):
self.data.item.put_card(
user_id,
user_card["cardId"],
user_card["cardTypeId"],
user_card["charaId"],
user_card["mapId"],
# add the correct start date and also the end date in 15 days
start_date,
end_date,
)
# if no pass is already added, add the corresponding pass
if not user_card["cardTypeId"] in selected_cards:
selected_cards.insert(0, user_card["cardTypeId"])
# get the profile extend to save the new bought card
extend = self.data.profile.get_profile_extend(user_id, self.version)
if extend:
extend = extend._asdict()
# parse the selectedCardList
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
selected_cards: List = extend["selectedCardList"]
extend["selectedCardList"] = selected_cards
self.data.profile.put_profile_extend(user_id, self.version, extend)
# if no pass is already added, add the corresponding pass
if not user_card["cardTypeId"] in selected_cards:
selected_cards.insert(0, user_card["cardTypeId"])
# properly format userPrintDetail for the database
upsert.pop("userCard")
upsert.pop("serialId")
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
extend["selectedCardList"] = selected_cards
self.data.profile.put_profile_extend(user_id, self.version, extend)
self.data.item.put_user_print_detail(user_id, serial_id, upsert)
# properly format userPrintDetail for the database
upsert.pop("userCard")
upsert.pop("serialId")
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
self.data.item.put_user_print_detail(user_id, serial_id, upsert)
return {
"returnCode": 1,

View File

@ -3,6 +3,7 @@ from typing import Any, Dict, List
import json
import logging
from enum import Enum
from contextlib import nullcontext
from core.config import CoreConfig
from core.data.cache import cached
@ -852,125 +853,127 @@ class OngekiBase:
user_id = data["userId"]
# The isNew fields are new as of Red and up. We just won't use them for now.
if "userData" in upsert and len(upsert["userData"]) > 0:
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(user_id, playlog)
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert and len(upsert["userData"]) > 0:
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
if "userBpBaseList" in upsert:
self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"])
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
)
if "userMusicDetailList" in upsert:
for x in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, x)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userDeckList" in upsert:
for x in upsert["userDeckList"]:
self.data.item.put_deck(user_id, x)
if "userTrainingRoomList" in upsert:
for x in upsert["userTrainingRoomList"]:
self.data.profile.put_training_room(user_id, x)
if "userStoryList" in upsert:
for x in upsert["userStoryList"]:
self.data.item.put_story(user_id, x)
if "userChapterList" in upsert:
for x in upsert["userChapterList"]:
self.data.item.put_chapter(user_id, x)
if "userMemoryChapterList" in upsert:
for x in upsert["userMemoryChapterList"]:
self.data.item.put_memorychapter(user_id, x)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userMusicItemList" in upsert:
for x in upsert["userMusicItemList"]:
self.data.item.put_music_item(user_id, x)
if "userLoginBonusList" in upsert:
for x in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(user_id, x)
if "userEventPointList" in upsert:
for x in upsert["userEventPointList"]:
self.data.item.put_event_point(user_id, x)
if "userMissionPointList" in upsert:
for x in upsert["userMissionPointList"]:
self.data.item.put_mission_point(user_id, x)
if "userRatinglogList" in upsert:
for x in upsert["userRatinglogList"]:
self.data.profile.put_profile_rating_log(
user_id, x["dataVersion"], x["highestRating"]
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userBossList" in upsert:
for x in upsert["userBossList"]:
self.data.item.put_boss(user_id, x)
if "userBpBaseList" in upsert:
self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"])
if "userTechCountList" in upsert:
for x in upsert["userTechCountList"]:
self.data.score.put_tech_count(user_id, x)
if "userTrainingRoomList" in upsert:
for x in upsert["userTrainingRoomList"]:
self.data.profile.put_training_room(user_id, x)
if "userScenerioList" in upsert:
for x in upsert["userScenerioList"]:
self.data.item.put_scenerio(user_id, x)
if "userRatinglogList" in upsert:
for x in upsert["userRatinglogList"]:
self.data.profile.put_profile_rating_log(
user_id, x["dataVersion"], x["highestRating"]
)
if "userTradeItemList" in upsert:
for x in upsert["userTradeItemList"]:
self.data.item.put_trade_item(user_id, x)
if "userKopList" in upsert:
for x in upsert["userKopList"]:
self.data.profile.put_kop(user_id, x)
if "userEventMusicList" in upsert:
for x in upsert["userEventMusicList"]:
self.data.item.put_event_music(user_id, x)
with self.data.score.conn.begin() or nullcontext():
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(user_id, playlog)
if "userTechEventList" in upsert:
for x in upsert["userTechEventList"]:
self.data.item.put_tech_event(user_id, x)
if "userMusicDetailList" in upsert:
for x in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, x)
if "userKopList" in upsert:
for x in upsert["userKopList"]:
self.data.profile.put_kop(user_id, x)
if "userTechCountList" in upsert:
for x in upsert["userTechCountList"]:
self.data.score.put_tech_count(user_id, x)
with self.data.item.conn.begin() or nullcontext():
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userDeckList" in upsert:
for x in upsert["userDeckList"]:
self.data.item.put_deck(user_id, x)
if "userStoryList" in upsert:
for x in upsert["userStoryList"]:
self.data.item.put_story(user_id, x)
if "userChapterList" in upsert:
for x in upsert["userChapterList"]:
self.data.item.put_chapter(user_id, x)
if "userMemoryChapterList" in upsert:
for x in upsert["userMemoryChapterList"]:
self.data.item.put_memorychapter(user_id, x)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userMusicItemList" in upsert:
for x in upsert["userMusicItemList"]:
self.data.item.put_music_item(user_id, x)
if "userLoginBonusList" in upsert:
for x in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(user_id, x)
if "userEventPointList" in upsert:
for x in upsert["userEventPointList"]:
self.data.item.put_event_point(user_id, x)
if "userMissionPointList" in upsert:
for x in upsert["userMissionPointList"]:
self.data.item.put_mission_point(user_id, x)
if "userBossList" in upsert:
for x in upsert["userBossList"]:
self.data.item.put_boss(user_id, x)
if "userScenerioList" in upsert:
for x in upsert["userScenerioList"]:
self.data.item.put_scenerio(user_id, x)
if "userTradeItemList" in upsert:
for x in upsert["userTradeItemList"]:
self.data.item.put_trade_item(user_id, x)
if "userEventMusicList" in upsert:
for x in upsert["userEventMusicList"]:
self.data.item.put_event_music(user_id, x)
if "userTechEventList" in upsert:
for x in upsert["userTechEventList"]:
self.data.item.put_tech_event(user_id, x)
return {"returnCode": 1, "apiName": "upsertUserAll"}

View File

@ -3,6 +3,7 @@ from typing import Any, Dict
from random import randint
import pytz
import json
from contextlib import nullcontext
from core.config import CoreConfig
from titles.ongeki.base import OngekiBase
@ -358,46 +359,50 @@ class OngekiBright(OngekiBase):
daily_gacha_date = play_date
daily_gacha_cnt = 0
self.data.item.put_user_gacha(
user_id,
gacha_id,
totalGachaCnt=total_gacha_count + gacha_count,
ceilingGachaCnt=ceiling_gacha_count + gacha_count,
selectPoint=select_point,
useSelectPoint=use_select_point,
dailyGachaCnt=daily_gacha_cnt + gacha_count,
fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt,
elevenGachaCnt=eleven_gacha_cnt + 1
if gacha_count == 11
else eleven_gacha_cnt,
dailyGachaDate=daily_gacha_date,
)
with (
self.data.item.conn.begin() or nullcontext(),
self.data.profile.conn.begin() or nullcontext(),
):
self.data.item.put_user_gacha(
user_id,
gacha_id,
totalGachaCnt=total_gacha_count + gacha_count,
ceilingGachaCnt=ceiling_gacha_count + gacha_count,
selectPoint=select_point,
useSelectPoint=use_select_point,
dailyGachaCnt=daily_gacha_cnt + gacha_count,
fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt,
elevenGachaCnt=eleven_gacha_cnt + 1
if gacha_count == 11
else eleven_gacha_cnt,
dailyGachaDate=daily_gacha_date,
)
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
# TODO?
# if "gameGachaCardList" in upsert:
@ -409,36 +414,38 @@ class OngekiBright(OngekiBase):
upsert = data["cmUpsertUserSelectGacha"]
user_id = data["userId"]
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
with self.data.item.conn.begin() or nullcontext():
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "selectGachaLogList" in data:
for x in data["selectGachaLogList"]:
self.data.item.put_user_gacha(
user_id,
x["gachaId"],
selectPoint=0,
useSelectPoint=x["useSelectPoint"],
)
if "selectGachaLogList" in data:
for x in data["selectGachaLogList"]:
self.data.item.put_user_gacha(
user_id,
x["gachaId"],
selectPoint=0,
useSelectPoint=x["useSelectPoint"],
)
return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"}
@ -593,39 +600,41 @@ class OngekiBright(OngekiBase):
upsert = data["cmUpsertUserAll"]
user_id = data["userId"]
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
with self.data.profile.conn.begin() or nullcontext():
if "userData" in upsert and len(upsert["userData"]) > 0:
# check if the profile is a bright memory profile
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
)
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
with self.data.item.conn.begin() or nullcontext():
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
return {"returnCode": 1, "apiName": "cmUpsertUserAll"}

View File

@ -139,9 +139,7 @@ class OngekiServlet:
req_data = json.loads(unzip)
self.logger.info(
f"v{version} {endpoint} request from {client_ip}"
)
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
self.logger.debug(req_data)
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"

View File

@ -4,6 +4,7 @@ import os
import re
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional
from contextlib import nullcontext
from read import BaseReader
from core.config import CoreConfig
@ -41,9 +42,10 @@ class OngekiReader(BaseReader):
data_dirs += self.get_data_directories(self.opt_dir)
for dir in data_dirs:
self.read_events(f"{dir}/event")
self.read_music(f"{dir}/music")
self.read_card(f"{dir}/card")
with self.data.static.conn.begin() or nullcontext():
self.read_events(f"{dir}/event")
self.read_music(f"{dir}/music")
self.read_card(f"{dir}/card")
def read_card(self, base_dir: str) -> None:
self.logger.info(f"Reading cards from {base_dir}...")

View File

@ -1,13 +1,13 @@
from titles.ongeki.schema.profile import OngekiProfileData
from titles.ongeki.schema.item import OngekiItemData
from titles.ongeki.schema.static import OngekiStaticData
from titles.ongeki.schema.score import OngekiScoreData
from titles.ongeki.schema.log import OngekiLogData
__all__ = [
OngekiProfileData,
OngekiItemData,
OngekiStaticData,
OngekiScoreData,
OngekiLogData,
]
from titles.ongeki.schema.profile import OngekiProfileData
from titles.ongeki.schema.item import OngekiItemData
from titles.ongeki.schema.static import OngekiStaticData
from titles.ongeki.schema.score import OngekiScoreData
from titles.ongeki.schema.log import OngekiLogData
__all__ = [
OngekiProfileData,
OngekiItemData,
OngekiStaticData,
OngekiScoreData,
OngekiLogData,
]

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +1,69 @@
from typing import Dict, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
gp_log = Table(
"ongeki_gp_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("usedCredit", Integer),
Column("placeName", String(255)),
Column("trxnDate", String(255)),
Column(
"placeId", Integer
), # Making this an FK would mess with people playing with default KC
Column("kind", Integer),
Column("pattern", Integer),
Column("currentGP", Integer),
mysql_charset="utf8mb4",
)
session_log = Table(
"ongeki_session_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("playDate", String(10)),
Column("userPlayDate", String(25)),
Column("isPaid", Boolean),
mysql_charset="utf8mb4",
)
class OngekiLogData(BaseData):
def put_gp_log(
self,
aime_id: Optional[int],
used_credit: int,
place_name: str,
tx_date: str,
place_id: int,
kind: int,
pattern: int,
current_gp: int,
) -> Optional[int]:
sql = insert(gp_log).values(
user=aime_id,
usedCredit=used_credit,
placeName=place_name,
trxnDate=tx_date,
placeId=place_id,
kind=kind,
pattern=pattern,
currentGP=current_gp,
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}"
)
return result.lastrowid
from typing import Dict, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
gp_log = Table(
"ongeki_gp_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("usedCredit", Integer),
Column("placeName", String(255)),
Column("trxnDate", String(255)),
Column(
"placeId", Integer
), # Making this an FK would mess with people playing with default KC
Column("kind", Integer),
Column("pattern", Integer),
Column("currentGP", Integer),
mysql_charset="utf8mb4",
)
session_log = Table(
"ongeki_session_log",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("playDate", String(10)),
Column("userPlayDate", String(25)),
Column("isPaid", Boolean),
mysql_charset="utf8mb4",
)
class OngekiLogData(BaseData):
def put_gp_log(
self,
aime_id: Optional[int],
used_credit: int,
place_name: str,
tx_date: str,
place_id: int,
kind: int,
pattern: int,
current_gp: int,
) -> Optional[int]:
sql = insert(gp_log).values(
user=aime_id,
usedCredit=used_credit,
placeName=place_name,
trxnDate=tx_date,
placeId=place_id,
kind=kind,
pattern=pattern,
currentGP=current_gp,
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}"
)
return result.lastrowid

File diff suppressed because it is too large Load Diff

View File

@ -1,180 +1,180 @@
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
score_best = Table(
"ongeki_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer, nullable=False),
Column("level", Integer, nullable=False),
Column("playCount", Integer, nullable=False),
Column("techScoreMax", Integer, nullable=False),
Column("techScoreRank", Integer, nullable=False),
Column("battleScoreMax", Integer, nullable=False),
Column("battleScoreRank", Integer, nullable=False),
Column("maxComboCount", Integer, nullable=False),
Column("maxOverKill", Float, nullable=False),
Column("maxTeamOverKill", Float, nullable=False),
Column("isFullBell", Boolean, nullable=False),
Column("isFullCombo", Boolean, nullable=False),
Column("isAllBreake", Boolean, nullable=False),
Column("isLock", Boolean, nullable=False),
Column("clearStatus", Boolean, nullable=False),
Column("isStoryWatched", Boolean, nullable=False),
Column("platinumScoreMax", Integer),
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"ongeki_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("playDate", TIMESTAMP),
Column("userPlayDate", TIMESTAMP),
Column("musicId", Integer),
Column("level", Integer),
Column("playKind", Integer),
Column("eventId", Integer),
Column("eventName", String(255)),
Column("eventPoint", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(8)),
Column("playedUserName2", String(8)),
Column("playedUserName3", String(8)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("cardId1", Integer),
Column("cardId2", Integer),
Column("cardId3", Integer),
Column("cardLevel1", Integer),
Column("cardLevel2", Integer),
Column("cardLevel3", Integer),
Column("cardAttack1", Integer),
Column("cardAttack2", Integer),
Column("cardAttack3", Integer),
Column("bossCharaId", Integer),
Column("bossLevel", Integer),
Column("bossAttribute", Integer),
Column("clearStatus", Integer),
Column("techScore", Integer),
Column("techScoreRank", Integer),
Column("battleScore", Integer),
Column("battleScoreRank", Integer),
Column("maxCombo", Integer),
Column("judgeMiss", Integer),
Column("judgeHit", Integer),
Column("judgeBreak", Integer),
Column("judgeCriticalBreak", Integer),
Column("rateTap", Integer),
Column("rateHold", Integer),
Column("rateFlick", Integer),
Column("rateSideTap", Integer),
Column("rateSideHold", Integer),
Column("bellCount", Integer),
Column("totalBellCount", Integer),
Column("damageCount", Integer),
Column("overDamage", Integer),
Column("isTechNewRecord", Boolean),
Column("isBattleNewRecord", Boolean),
Column("isOverDamageNewRecord", Boolean),
Column("isFullCombo", Boolean),
Column("isFullBell", Boolean),
Column("isAllBreak", Boolean),
Column("playerRating", Integer),
Column("battlePoint", Integer),
Column("platinumScore", Integer),
Column("platinumScoreMax", Integer),
mysql_charset="utf8mb4",
)
tech_count = Table(
"ongeki_score_tech_count",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("levelId", Integer, nullable=False),
Column("allBreakCount", Integer),
Column("allBreakPlusCount", Integer),
UniqueConstraint("user", "levelId", name="ongeki_tech_count_uk"),
mysql_charset="utf8mb4",
)
class OngekiScoreData(BaseData):
def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]:
return []
def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]:
tech_count_data["user"] = aime_id
sql = insert(tech_count).values(**tech_count_data)
conflict = sql.on_duplicate_key_update(**tech_count_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]:
sql = select(score_best).where(score_best.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_score(
self, aime_id: int, song_id: int, chart_id: int = None
) -> Optional[List[Dict]]:
return []
def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]:
music_detail["user"] = aime_id
sql = insert(score_best).values(**music_detail)
conflict = sql.on_duplicate_key_update(**music_detail)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None
return result.lastrowid
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]:
playlog_data["user"] = aime_id
sql = insert(playlog).values(**playlog_data)
result = self.execute(sql)
if result is None:
self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}")
return None
return result.lastrowid
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
score_best = Table(
"ongeki_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer, nullable=False),
Column("level", Integer, nullable=False),
Column("playCount", Integer, nullable=False),
Column("techScoreMax", Integer, nullable=False),
Column("techScoreRank", Integer, nullable=False),
Column("battleScoreMax", Integer, nullable=False),
Column("battleScoreRank", Integer, nullable=False),
Column("maxComboCount", Integer, nullable=False),
Column("maxOverKill", Float, nullable=False),
Column("maxTeamOverKill", Float, nullable=False),
Column("isFullBell", Boolean, nullable=False),
Column("isFullCombo", Boolean, nullable=False),
Column("isAllBreake", Boolean, nullable=False),
Column("isLock", Boolean, nullable=False),
Column("clearStatus", Boolean, nullable=False),
Column("isStoryWatched", Boolean, nullable=False),
Column("platinumScoreMax", Integer),
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"ongeki_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("playDate", TIMESTAMP),
Column("userPlayDate", TIMESTAMP),
Column("musicId", Integer),
Column("level", Integer),
Column("playKind", Integer),
Column("eventId", Integer),
Column("eventName", String(255)),
Column("eventPoint", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(8)),
Column("playedUserName2", String(8)),
Column("playedUserName3", String(8)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("cardId1", Integer),
Column("cardId2", Integer),
Column("cardId3", Integer),
Column("cardLevel1", Integer),
Column("cardLevel2", Integer),
Column("cardLevel3", Integer),
Column("cardAttack1", Integer),
Column("cardAttack2", Integer),
Column("cardAttack3", Integer),
Column("bossCharaId", Integer),
Column("bossLevel", Integer),
Column("bossAttribute", Integer),
Column("clearStatus", Integer),
Column("techScore", Integer),
Column("techScoreRank", Integer),
Column("battleScore", Integer),
Column("battleScoreRank", Integer),
Column("maxCombo", Integer),
Column("judgeMiss", Integer),
Column("judgeHit", Integer),
Column("judgeBreak", Integer),
Column("judgeCriticalBreak", Integer),
Column("rateTap", Integer),
Column("rateHold", Integer),
Column("rateFlick", Integer),
Column("rateSideTap", Integer),
Column("rateSideHold", Integer),
Column("bellCount", Integer),
Column("totalBellCount", Integer),
Column("damageCount", Integer),
Column("overDamage", Integer),
Column("isTechNewRecord", Boolean),
Column("isBattleNewRecord", Boolean),
Column("isOverDamageNewRecord", Boolean),
Column("isFullCombo", Boolean),
Column("isFullBell", Boolean),
Column("isAllBreak", Boolean),
Column("playerRating", Integer),
Column("battlePoint", Integer),
Column("platinumScore", Integer),
Column("platinumScoreMax", Integer),
mysql_charset="utf8mb4",
)
tech_count = Table(
"ongeki_score_tech_count",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("levelId", Integer, nullable=False),
Column("allBreakCount", Integer),
Column("allBreakPlusCount", Integer),
UniqueConstraint("user", "levelId", name="ongeki_tech_count_uk"),
mysql_charset="utf8mb4",
)
class OngekiScoreData(BaseData):
def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]:
return []
def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]:
tech_count_data["user"] = aime_id
sql = insert(tech_count).values(**tech_count_data)
conflict = sql.on_conflict_do_update(set_=dict(**tech_count_data))
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]:
sql = select(score_best).where(score_best.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_score(
self, aime_id: int, song_id: int, chart_id: int = None
) -> Optional[List[Dict]]:
return []
def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]:
music_detail["user"] = aime_id
sql = insert(score_best).values(**music_detail)
conflict = sql.on_conflict_do_update(set_=dict(**music_detail))
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None
return result.lastrowid
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]:
playlog_data["user"] = aime_id
sql = insert(playlog).values(**playlog_data)
result = self.execute(sql)
if result is None:
self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}")
return None
return result.lastrowid

View File

@ -1,335 +1,341 @@
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
events = Table(
"ongeki_static_events",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"),
mysql_charset="utf8mb4",
)
music = Table(
"ongeki_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("genre", String(255)),
Column("level", Float),
UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"),
mysql_charset="utf8mb4",
)
gachas = Table(
"ongeki_static_gachas",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("gachaName", String(255), nullable=False),
Column("type", Integer, nullable=False, server_default="0"),
Column("kind", Integer, nullable=False, server_default="0"),
Column("isCeiling", Boolean, server_default="0"),
Column("maxSelectPoint", Integer, server_default="0"),
Column("ceilingCnt", Integer, server_default="10"),
Column("changeRateCnt1", Integer, server_default="0"),
Column("changeRateCnt2", Integer, server_default="0"),
Column("startDate", TIMESTAMP, server_default="2018-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("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"),
mysql_charset="utf8mb4",
)
gacha_cards = Table(
"ongeki_static_gacha_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("rarity", Integer, nullable=False),
Column("weight", Integer, server_default="1"),
Column("isPickup", Boolean, server_default="0"),
Column("isSelect", Boolean, server_default="0"),
UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"),
mysql_charset="utf8mb4",
)
cards = Table(
"ongeki_static_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("name", String(255), nullable=False),
Column("charaId", Integer, nullable=False),
Column("nickName", String(255)),
Column("school", String(255), nullable=False),
Column("attribute", String(5), nullable=False),
Column("gakunen", String(255), nullable=False),
Column("rarity", Integer, nullable=False),
Column("levelParam", String(255), nullable=False),
Column("skillId", Integer, nullable=False),
Column("choKaikaSkillId", Integer, nullable=False),
Column("cardNumber", String(255)),
UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"),
mysql_charset="utf8mb4",
)
class OngekiStaticData(BaseData):
def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
sql = insert(cards).values(version=version, cardId=card_id, **card_data)
conflict = sql.on_duplicate_key_update(**card_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}")
return None
return result.lastrowid
def get_card(self, version: int, card_id: int) -> Optional[Dict]:
sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]:
if not card_number.startswith("[O.N.G.E.K.I.]"):
card_number = f"[O.N.G.E.K.I.]{card_number}"
sql = cards.select(
and_(cards.c.version <= version, cards.c.cardNumber == card_number)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_name(self, version: int, name: str) -> Optional[Dict]:
sql = cards.select(and_(cards.c.version <= version, cards.c.name == name))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_cards(self, version: int) -> Optional[List[Dict]]:
sql = cards.select(cards.c.version <= version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]:
sql = cards.select(and_(cards.c.version <= version, cards.c.rarity == rarity))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_gacha(
self,
version: int,
gacha_id: int,
gacha_name: int,
gacha_kind: int,
**gacha_data,
) -> Optional[int]:
sql = insert(gachas).values(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
kind=gacha_kind,
**gacha_data,
)
conflict = sql.on_duplicate_key_update(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
kind=gacha_kind,
**gacha_data,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
sql = gachas.select(
and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_gachas(self, version: int) -> Optional[List[Dict]]:
sql = gachas.select(gachas.c.version == version).order_by(
gachas.c.gachaId.asc()
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_gacha_card(
self, gacha_id: int, card_id: int, **gacha_card
) -> Optional[int]:
sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card)
conflict = sql.on_duplicate_key_update(
gachaId=gacha_id, cardId=card_id, **gacha_card
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_event(
self, version: int, event_id: int, event_type: int, event_name: str
) -> Optional[int]:
sql = insert(events).values(
version=version,
eventId=event_id,
type=event_type,
name=event_name,
)
conflict = sql.on_duplicate_key_update(
name=event_name,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert event! event_id {event_id}")
return None
return result.lastrowid
def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]:
sql = select(events).where(
and_(events.c.version == version, events.c.eventId == event_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_events(self, version: int) -> Optional[List[Dict]]:
sql = select(events).where(events.c.version == version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_enabled_events(self, version: int) -> Optional[List[Dict]]:
sql = select(events).where(
and_(events.c.version == version, events.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_chart(
self,
version: int,
song_id: int,
chart_id: int,
title: str,
artist: str,
genre: str,
level: float,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
genre=genre,
level=level,
)
conflict = sql.on_duplicate_key_update(
title=title,
artist=artist,
genre=genre,
level=level,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}"
)
return None
return result.lastrowid
def get_chart(
self, version: int, song_id: int, chart_id: int = None
) -> Optional[List[Dict]]:
pass
def get_music(self, version: int) -> Optional[List[Dict]]:
pass
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
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.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
events = Table(
"ongeki_static_events",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"),
mysql_charset="utf8mb4",
)
music = Table(
"ongeki_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("genre", String(255)),
Column("level", Float),
UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"),
mysql_charset="utf8mb4",
)
gachas = Table(
"ongeki_static_gachas",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("gachaName", String(255), nullable=False),
Column("type", Integer, nullable=False, server_default="0"),
Column("kind", Integer, nullable=False, server_default="0"),
Column("isCeiling", Boolean, server_default="0"),
Column("maxSelectPoint", Integer, server_default="0"),
Column("ceilingCnt", Integer, server_default="10"),
Column("changeRateCnt1", Integer, server_default="0"),
Column("changeRateCnt2", Integer, server_default="0"),
Column("startDate", TIMESTAMP, server_default="2018-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("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"),
mysql_charset="utf8mb4",
)
gacha_cards = Table(
"ongeki_static_gacha_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("rarity", Integer, nullable=False),
Column("weight", Integer, server_default="1"),
Column("isPickup", Boolean, server_default="0"),
Column("isSelect", Boolean, server_default="0"),
UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"),
mysql_charset="utf8mb4",
)
cards = Table(
"ongeki_static_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("name", String(255), nullable=False),
Column("charaId", Integer, nullable=False),
Column("nickName", String(255)),
Column("school", String(255), nullable=False),
Column("attribute", String(5), nullable=False),
Column("gakunen", String(255), nullable=False),
Column("rarity", Integer, nullable=False),
Column("levelParam", String(255), nullable=False),
Column("skillId", Integer, nullable=False),
Column("choKaikaSkillId", Integer, nullable=False),
Column("cardNumber", String(255)),
UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"),
mysql_charset="utf8mb4",
)
class OngekiStaticData(BaseData):
def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
sql = insert(cards).values(version=version, cardId=card_id, **card_data)
conflict = sql.on_conflict_do_update(set_=dict(**card_data))
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}")
return None
return result.lastrowid
def get_card(self, version: int, card_id: int) -> Optional[Dict]:
sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]:
if not card_number.startswith("[O.N.G.E.K.I.]"):
card_number = f"[O.N.G.E.K.I.]{card_number}"
sql = cards.select(
and_(cards.c.version <= version, cards.c.cardNumber == card_number)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_card_by_name(self, version: int, name: str) -> Optional[Dict]:
sql = cards.select(and_(cards.c.version <= version, cards.c.name == name))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_cards(self, version: int) -> Optional[List[Dict]]:
sql = cards.select(cards.c.version <= version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]:
sql = cards.select(and_(cards.c.version <= version, cards.c.rarity == rarity))
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_gacha(
self,
version: int,
gacha_id: int,
gacha_name: int,
gacha_kind: int,
**gacha_data,
) -> Optional[int]:
sql = insert(gachas).values(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
kind=gacha_kind,
**gacha_data,
)
conflict = sql.on_conflict_do_update(
set_=dict(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
kind=gacha_kind,
**gacha_data,
)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
sql = gachas.select(
and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_gachas(self, version: int) -> Optional[List[Dict]]:
sql = gachas.select(gachas.c.version == version).order_by(
gachas.c.gachaId.asc()
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_gacha_card(
self, gacha_id: int, card_id: int, **gacha_card
) -> Optional[int]:
sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card)
conflict = sql.on_conflict_do_update(
set_=dict(gachaId=gacha_id, cardId=card_id, **gacha_card)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_event(
self, version: int, event_id: int, event_type: int, event_name: str
) -> Optional[int]:
sql = insert(events).values(
version=version,
eventId=event_id,
type=event_type,
name=event_name,
)
conflict = sql.on_conflict_do_update(
set_=dict(
name=event_name,
)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert event! event_id {event_id}")
return None
return result.lastrowid
def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]:
sql = select(events).where(
and_(events.c.version == version, events.c.eventId == event_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_events(self, version: int) -> Optional[List[Dict]]:
sql = select(events).where(events.c.version == version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_enabled_events(self, version: int) -> Optional[List[Dict]]:
sql = select(events).where(
and_(events.c.version == version, events.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_chart(
self,
version: int,
song_id: int,
chart_id: int,
title: str,
artist: str,
genre: str,
level: float,
) -> Optional[int]:
sql = insert(music).values(
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
genre=genre,
level=level,
)
conflict = sql.on_conflict_do_update(
set_=dict(
title=title,
artist=artist,
genre=genre,
level=level,
)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}"
)
return None
return result.lastrowid
def get_chart(
self, version: int, song_id: int, chart_id: int = None
) -> Optional[List[Dict]]:
pass
def get_music(self, version: int) -> Optional[List[Dict]]:
pass
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta
import json, logging
from typing import Any, Dict, List
import random
from contextlib import nullcontext
from core.data import Data
from core import CoreConfig
@ -57,7 +58,7 @@ class PokkenBase:
"port": self.game_cfg.server.stun_server_port,
},
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}",
"locationId": 123, # FIXME: Get arcade's ID from the database
"locationId": 123, # FIXME: Get arcade's ID from the database
"logfilename": "JackalMatchingLibrary.log",
"biwalogfilename": "./biwa.log",
}
@ -147,7 +148,7 @@ class PokkenBase:
res.load_user.CopyFrom(load_usr)
return res.SerializeToString()
"""
"""
TODO: Add repeated values
tutorial_progress_flag
rankmatch_progress
@ -278,13 +279,15 @@ class PokkenBase:
req = request.save_user
user_id = req.banapass_id
tut_flgs: List[int] = []
ach_flgs: List[int] = []
evt_flgs: List[int] = []
evt_params: List[int] = []
get_rank_pts: int = req.get_trainer_rank_point if req.get_trainer_rank_point else 0
get_rank_pts: int = (
req.get_trainer_rank_point if req.get_trainer_rank_point else 0
)
get_money: int = req.get_money
get_score_pts: int = req.get_score_point if req.get_score_point else 0
grade_max: int = req.grade_max_num
@ -294,7 +297,7 @@ class PokkenBase:
total_play_days: int = req.total_play_days
awake_num: int = req.awake_num # ?
use_support_ct: int = req.use_support_num
beat_num: int = req.beat_num # ?
beat_num: int = req.beat_num # ?
evt_state: int = req.event_state
aid_skill: int = req.aid_skill
last_evt: int = req.last_play_event_id
@ -302,73 +305,106 @@ class PokkenBase:
battle = req.battle_data
mon = req.pokemon_data
p = self.data.profile.touch_profile(user_id)
if p is None or not p:
self.data.profile.create_profile(user_id)
with self.data.profile.conn.begin() or nullcontext():
p = self.data.profile.touch_profile(user_id)
if p is None or not p:
self.data.profile.create_profile(user_id)
if req.trainer_name_pending is not None and req.trainer_name_pending: # we're saving for the first time
self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None)
if (
req.trainer_name_pending is not None and req.trainer_name_pending
): # we're saving for the first time
self.data.profile.set_profile_name(
user_id,
req.trainer_name_pending,
req.avatar_gender if req.avatar_gender else None,
)
for tut_flg in req.tutorial_progress_flag:
tut_flgs.append(tut_flg)
self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs)
for tut_flg in req.tutorial_progress_flag:
tut_flgs.append(tut_flg)
for ach_flg in req.achievement_flag:
ach_flgs.append(ach_flg)
self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs)
for evt_flg in req.event_achievement_flag:
evt_flgs.append(evt_flg)
for ach_flg in req.achievement_flag:
ach_flgs.append(ach_flg)
for evt_param in req.event_achievement_param:
evt_params.append(evt_param)
self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, )
for reward in req.reward_data:
self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id)
self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max)
self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])
self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1])
for evt_flg in req.event_achievement_flag:
evt_flgs.append(evt_flg)
self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp)
self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp)
for x in range(len(battle.play_mode)):
self.data.profile.put_pokemon_battle_result(
user_id,
mon.char_id,
PokkenConstants.BATTLE_TYPE(battle.play_mode[x]),
PokkenConstants.BATTLE_RESULT(battle.result[x])
for evt_param in req.event_achievement_param:
evt_params.append(evt_param)
self.data.profile.update_profile_event(
user_id,
evt_state,
evt_flgs,
evt_params,
)
self.data.profile.put_stats(
user_id,
battle.ex_ko_num,
battle.wko_num,
battle.timeup_win_num,
battle.cool_ko_num,
battle.perfect_ko_num,
num_continues
)
self.data.profile.add_profile_points(
user_id, get_rank_pts, get_money, get_score_pts, grade_max
)
self.data.profile.put_extra(
user_id,
extra_counter,
evt_reward_get_flg,
total_play_days,
awake_num,
use_support_ct,
beat_num,
aid_skill,
last_evt
)
self.data.profile.update_support_team(
user_id, 1, req.support_set_1[0], req.support_set_1[1]
)
self.data.profile.update_support_team(
user_id, 2, req.support_set_2[0], req.support_set_2[1]
)
self.data.profile.update_support_team(
user_id, 3, req.support_set_3[0], req.support_set_3[1]
)
self.data.profile.put_pokemon(
user_id,
mon.char_id,
mon.illustration_book_no,
mon.bp_point_atk,
mon.bp_point_res,
mon.bp_point_def,
mon.bp_point_sp,
)
self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp)
for x in range(len(battle.play_mode)):
self.data.profile.put_pokemon_battle_result(
user_id,
mon.char_id,
PokkenConstants.BATTLE_TYPE(battle.play_mode[x]),
PokkenConstants.BATTLE_RESULT(battle.result[x]),
)
self.data.profile.put_stats(
user_id,
battle.ex_ko_num,
battle.wko_num,
battle.timeup_win_num,
battle.cool_ko_num,
battle.perfect_ko_num,
num_continues,
)
self.data.profile.put_extra(
user_id,
extra_counter,
evt_reward_get_flg,
total_play_days,
awake_num,
use_support_ct,
beat_num,
aid_skill,
last_evt,
)
with self.data.item.conn.begin() or nullcontext():
for reward in req.reward_data:
self.data.item.add_reward(
user_id,
reward.get_category_id,
reward.get_content_id,
reward.get_type_id,
)
return res.SerializeToString()
@ -398,20 +434,17 @@ class PokkenBase:
self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict:
"""
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"""
return {
"data": {
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"list":[]
"sessionId": "12345678",
"A": {"pcb_id": data["data"]["must"]["pcb_id"], "gip": client_ip},
"list": [],
}
}
@ -422,11 +455,9 @@ class PokkenBase:
def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
return {}
def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
def handle_admission_joinsession(
self, data: Dict, req_ip: str = "127.0.0.1"
) -> Dict:
self.logger.info(f"Admission: JoinSession from {req_ip}")
return {
'data': {
"id": 12345678
}
}
return {"data": {"id": 12345678}}

View File

@ -43,14 +43,18 @@ class PokkenServerConfig:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "enable_matching", default=False
)
@property
def stun_server_host(self) -> str:
"""
Hostname of the EXTERNAL stun server the game should connect to. This is not handled by artemis.
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org"
self.__config,
"pokken",
"server",
"stun_server_host",
default="stunserver.stunprotocol.org",
)
@property
@ -62,10 +66,11 @@ class PokkenServerConfig:
self.__config, "pokken", "server", "stun_server_port", default=3478
)
class PokkenPortsConfig:
def __init__(self, parent_config: "PokkenConfig"):
self.__config = parent_config
@property
def game(self) -> int:
return CoreConfig.get_config_field(
@ -77,7 +82,7 @@ class PokkenPortsConfig:
return CoreConfig.get_config_field(
self.__config, "pokken", "ports", "admission", default=9001
)
class PokkenConfig(dict):
def __init__(self) -> None:

View File

@ -35,5 +35,5 @@ class PokkenFrontend(FE_Base):
return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh)
sesh=vars(usr_sesh),
).encode("utf-16")

View File

@ -94,7 +94,8 @@ class PokkenServlet(resource.Resource):
def setup(self) -> None:
if self.game_cfg.server.enable_matching:
reactor.listenTCP(
self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg)
self.game_cfg.ports.admission,
PokkenAdmissionFactory(self.core_cfg, self.game_cfg),
)
def render_POST(
@ -134,7 +135,7 @@ class PokkenServlet(resource.Resource):
def handle_matching(self, request: Request) -> bytes:
if not self.game_cfg.server.enable_matching:
return b""
content = request.content.getvalue()
client_ip = Utils.get_ip_addr(request)

View File

@ -1,4 +1,4 @@
from .profile import PokkenProfileData
from .match import PokkenMatchData
from .item import PokkenItemData
from .static import PokkenStaticData
from .profile import PokkenProfileData
from .match import PokkenMatchData
from .item import PokkenItemData
from .static import PokkenStaticData

View File

@ -1,46 +1,50 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
item = Table(
"pokken_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("category", Integer),
Column("content", Integer),
Column("type", Integer),
UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"),
mysql_charset="utf8mb4",
)
class PokkenItemData(BaseData):
"""
Items obtained as rewards
"""
def add_reward(self, user_id: int, category: int, content: int, item_type: int) -> Optional[int]:
sql = insert(item).values(
user=user_id,
category=category,
content=content,
type=item_type,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}")
return None
return result.lastrowid
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
item = Table(
"pokken_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("category", Integer),
Column("content", Integer),
Column("type", Integer),
UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"),
mysql_charset="utf8mb4",
)
class PokkenItemData(BaseData):
"""
Items obtained as rewards
"""
def add_reward(
self, user_id: int, category: int, content: int, item_type: int
) -> Optional[int]:
sql = insert(item).values(
user=user_id,
category=category,
content=content,
type=item_type,
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}"
)
return None
return result.lastrowid

View File

@ -1,52 +1,52 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
# Pokken sends depressingly little match data...
match_data = Table(
"pokken_match_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("num_games", Integer),
Column("play_modes", JSON),
Column("results", JSON),
Column("ex_ko_num", Integer),
Column("wko_num", Integer),
Column("timeup_win_num", Integer),
Column("cool_ko_num", Integer),
Column("perfect_ko_num", Integer),
Column("use_navi", Integer),
Column("use_navi_cloth", Integer),
Column("use_aid_skill", Integer),
Column("play_date", TIMESTAMP),
mysql_charset="utf8mb4",
)
class PokkenMatchData(BaseData):
"""
Match logs
"""
def save_match(self, user_id: int, match_data: Dict) -> Optional[int]:
pass
def get_match(self, match_id: int) -> Optional[Row]:
pass
def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]:
pass
def get_matches(self, limit: int = 20) -> Optional[List[Row]]:
pass
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
# Pokken sends depressingly little match data...
match_data = Table(
"pokken_match_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("num_games", Integer),
Column("play_modes", JSON),
Column("results", JSON),
Column("ex_ko_num", Integer),
Column("wko_num", Integer),
Column("timeup_win_num", Integer),
Column("cool_ko_num", Integer),
Column("perfect_ko_num", Integer),
Column("use_navi", Integer),
Column("use_navi_cloth", Integer),
Column("use_aid_skill", Integer),
Column("play_date", TIMESTAMP),
mysql_charset="utf8mb4",
)
class PokkenMatchData(BaseData):
"""
Match logs
"""
def save_match(self, user_id: int, match_data: Dict) -> Optional[int]:
pass
def get_match(self, match_id: int) -> Optional[Row]:
pass
def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]:
pass
def get_matches(self, limit: int = 20) -> Optional[List[Row]]:
pass

View File

@ -1,362 +1,464 @@
from typing import Optional, Dict, List, Union
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from ..const import PokkenConstants
# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me
# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue
profile = Table(
"pokken_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("trainer_name", String(16)), # optional
Column("home_region_code", Integer),
Column("home_loc_name", String(255)),
Column("pref_code", Integer),
Column("navi_newbie_flag", Boolean),
Column("navi_enable_flag", Boolean),
Column("pad_vibrate_flag", Boolean),
Column("trainer_rank_point", Integer),
Column("wallet", Integer),
Column("fight_money", Integer),
Column("score_point", Integer),
Column("grade_max_num", Integer),
Column("extra_counter", Integer), # Optional
Column("tutorial_progress_flag", JSON), # Repeated, Integer
Column("total_play_days", Integer),
Column("play_date_time", Integer),
Column("achievement_flag", JSON), # Repeated, Integer
Column("lucky_box_fail_num", Integer),
Column("event_reward_get_flag", Integer),
Column("rank_pvp_all", Integer),
Column("rank_pvp_loc", Integer),
Column("rank_cpu_all", Integer),
Column("rank_cpu_loc", Integer),
Column("rank_event", Integer),
Column("awake_num", Integer),
Column("use_support_num", Integer),
Column("rankmatch_flag", Integer),
Column("rankmatch_max", Integer), # Optional
Column("rankmatch_progress", JSON), # Repeated, Integer
Column("rankmatch_success", Integer), # Optional
Column("beat_num", Integer), # Optional
Column("title_text_id", Integer),
Column("title_plate_id", Integer),
Column("title_decoration_id", Integer),
Column("support_pokemon_list", JSON), # Repeated, Integer
Column("support_set_1_1", Integer), # Repeated, Integer
Column("support_set_1_2", Integer),
Column("support_set_2_1", Integer), # Repeated, Integer
Column("support_set_2_2", Integer),
Column("support_set_3_1", Integer), # Repeated, Integer
Column("support_set_3_2", Integer),
Column("navi_trainer", Integer),
Column("navi_version_id", Integer),
Column("aid_skill_list", JSON), # Repeated, Integer
Column("aid_skill", Integer),
Column("comment_text_id", Integer),
Column("comment_word_id", Integer),
Column("latest_use_pokemon", Integer),
Column("ex_ko_num", Integer),
Column("wko_num", Integer),
Column("timeup_win_num", Integer),
Column("cool_ko_num", Integer),
Column("perfect_ko_num", Integer),
Column("record_flag", Integer),
Column("continue_num", Integer),
Column("avatar_body", Integer), # Optional
Column("avatar_gender", Integer), # Optional
Column("avatar_background", Integer), # Optional
Column("avatar_head", Integer), # Optional
Column("avatar_battleglass", Integer), # Optional
Column("avatar_face0", Integer), # Optional
Column("avatar_face1", Integer), # Optional
Column("avatar_face2", Integer), # Optional
Column("avatar_bodyall", Integer), # Optional
Column("avatar_wear", Integer), # Optional
Column("avatar_accessory", Integer), # Optional
Column("avatar_stamp", Integer), # Optional
Column("event_state", Integer),
Column("event_id", Integer),
Column("sp_bonus_category_id_1", Integer),
Column("sp_bonus_key_value_1", Integer),
Column("sp_bonus_category_id_2", Integer),
Column("sp_bonus_key_value_2", Integer),
Column("last_play_event_id", Integer), # Optional
Column("event_achievement_flag", JSON), # Repeated, Integer
Column("event_achievement_param", JSON), # Repeated, Integer
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_num_tutorial", Integer), # 1?
mysql_charset="utf8mb4",
)
pokemon_data = Table(
"pokken_pokemon_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("char_id", Integer, nullable=False),
Column("illustration_book_no", Integer),
Column("pokemon_exp", Integer),
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_all_num_tutorial", Integer), # ???
Column("battle_num_tutorial", Integer), # 1?
Column("bp_point_atk", Integer),
Column("bp_point_res", Integer),
Column("bp_point_def", Integer),
Column("bp_point_sp", Integer),
UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"),
mysql_charset="utf8mb4",
)
class PokkenProfileData(BaseData):
def touch_profile(self, user_id: int) -> Optional[int]:
sql = select([profile.c.id]).where(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()['id']
def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id)
conflict = sql.on_duplicate_key_update(user=user_id)
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to create pokken profile for user {user_id}!")
return None
return result.lastrowid
def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
trainer_name=new_name,
avatar_gender=gender if gender is not None else profile.c.avatar_gender
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile name for user {user_id}!"
)
def put_extra(
self,
user_id: int,
extra_counter: int,
evt_reward_get_flg: int,
total_play_days: int,
awake_num: int,
use_support_ct: int,
beat_num: int,
aid_skill: int,
last_evt: int
) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
extra_counter=extra_counter,
event_reward_get_flag=evt_reward_get_flg,
total_play_days=total_play_days,
awake_num=awake_num,
use_support_num=use_support_ct,
beat_num=beat_num,
aid_skill=aid_skill,
last_play_event_id=last_evt
)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to put extra data for user {user_id}")
def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
tutorial_progress_flag=tutorial_flags,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile tutorial flags for user {user_id}!"
)
def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
achievement_flag=achievement_flags,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile achievement flags for user {user_id}!"
)
def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
event_state=event_state,
event_achievement_flag=event_flags,
event_achievement_param=event_param,
last_play_event_id=last_evt if last_evt is not None else profile.c.last_play_event_id,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile event state for user {user_id}!"
)
def add_profile_points(
self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int
) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
trainer_rank_point = profile.c.trainer_rank_point + rank_pts,
fight_money = profile.c.fight_money + money,
score_point = profile.c.score_point + score_pts,
grade_max_num = grade_max
)
def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_pokemon(
self,
user_id: int,
pokemon_id: int,
illust_no: int,
atk: int,
res: int,
defe: int,
sp: int
) -> Optional[int]:
sql = insert(pokemon_data).values(
user=user_id,
char_id=pokemon_id,
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
conflict = sql.on_duplicate_key_update(
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}")
return None
return result.lastrowid
def add_pokemon_xp(
self,
user_id: int,
pokemon_id: int,
xp: int
) -> None:
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
pokemon_exp=pokemon_data.c.pokemon_exp + xp
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
pass
def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]:
pass
def put_pokemon_battle_result(
self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT
) -> None:
"""
Records the match stats (type and win/loss) for the pokemon and profile
"""
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
battle_num_tutorial=pokemon_data.c.battle_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_num_tutorial,
battle_all_num_tutorial=pokemon_data.c.battle_all_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_all_num_tutorial,
battle_num_vs_cpu=pokemon_data.c.battle_num_vs_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else pokemon_data.c.battle_num_vs_cpu,
win_cpu=pokemon_data.c.win_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_cpu,
battle_num_vs_lan=pokemon_data.c.battle_num_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else pokemon_data.c.battle_num_vs_lan,
win_vs_lan=pokemon_data.c.win_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_lan,
battle_num_vs_wan=pokemon_data.c.battle_num_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else pokemon_data.c.battle_num_vs_wan,
win_vs_wan=pokemon_data.c.win_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_wan,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})")
def put_stats(
self,
user_id: int,
exkos: int,
wkos: int,
timeout_wins: int,
cool_kos: int,
perfects: int,
continues: int,
) -> None:
"""
Records profile stats
"""
sql = update(profile).where(profile.c.user==user_id).values(
ex_ko_num=profile.c.ex_ko_num + exkos,
wko_num=profile.c.wko_num + wkos,
timeup_win_num=profile.c.timeup_win_num + timeout_wins,
cool_ko_num=profile.c.cool_ko_num + cool_kos,
perfect_ko_num=profile.c.perfect_ko_num + perfects,
continue_num=continues,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update stats for user {user_id}")
def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None:
sql = update(profile).where(profile.c.user==user_id).values(
support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1,
support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,
support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1,
support_set_2_2=support2 if support_id == 2 else profile.c.support_set_2_2,
support_set_3_1=support1 if support_id == 3 else profile.c.support_set_3_1,
support_set_3_2=support2 if support_id == 3 else profile.c.support_set_3_2,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update support team {support_id} for user {user_id}")
from typing import Optional, Dict, List, Union
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
from ..const import PokkenConstants
# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me
# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue
profile = Table(
"pokken_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("trainer_name", String(16)), # optional
Column("home_region_code", Integer),
Column("home_loc_name", String(255)),
Column("pref_code", Integer),
Column("navi_newbie_flag", Boolean),
Column("navi_enable_flag", Boolean),
Column("pad_vibrate_flag", Boolean),
Column("trainer_rank_point", Integer),
Column("wallet", Integer),
Column("fight_money", Integer),
Column("score_point", Integer),
Column("grade_max_num", Integer),
Column("extra_counter", Integer), # Optional
Column("tutorial_progress_flag", JSON), # Repeated, Integer
Column("total_play_days", Integer),
Column("play_date_time", Integer),
Column("achievement_flag", JSON), # Repeated, Integer
Column("lucky_box_fail_num", Integer),
Column("event_reward_get_flag", Integer),
Column("rank_pvp_all", Integer),
Column("rank_pvp_loc", Integer),
Column("rank_cpu_all", Integer),
Column("rank_cpu_loc", Integer),
Column("rank_event", Integer),
Column("awake_num", Integer),
Column("use_support_num", Integer),
Column("rankmatch_flag", Integer),
Column("rankmatch_max", Integer), # Optional
Column("rankmatch_progress", JSON), # Repeated, Integer
Column("rankmatch_success", Integer), # Optional
Column("beat_num", Integer), # Optional
Column("title_text_id", Integer),
Column("title_plate_id", Integer),
Column("title_decoration_id", Integer),
Column("support_pokemon_list", JSON), # Repeated, Integer
Column("support_set_1_1", Integer), # Repeated, Integer
Column("support_set_1_2", Integer),
Column("support_set_2_1", Integer), # Repeated, Integer
Column("support_set_2_2", Integer),
Column("support_set_3_1", Integer), # Repeated, Integer
Column("support_set_3_2", Integer),
Column("navi_trainer", Integer),
Column("navi_version_id", Integer),
Column("aid_skill_list", JSON), # Repeated, Integer
Column("aid_skill", Integer),
Column("comment_text_id", Integer),
Column("comment_word_id", Integer),
Column("latest_use_pokemon", Integer),
Column("ex_ko_num", Integer),
Column("wko_num", Integer),
Column("timeup_win_num", Integer),
Column("cool_ko_num", Integer),
Column("perfect_ko_num", Integer),
Column("record_flag", Integer),
Column("continue_num", Integer),
Column("avatar_body", Integer), # Optional
Column("avatar_gender", Integer), # Optional
Column("avatar_background", Integer), # Optional
Column("avatar_head", Integer), # Optional
Column("avatar_battleglass", Integer), # Optional
Column("avatar_face0", Integer), # Optional
Column("avatar_face1", Integer), # Optional
Column("avatar_face2", Integer), # Optional
Column("avatar_bodyall", Integer), # Optional
Column("avatar_wear", Integer), # Optional
Column("avatar_accessory", Integer), # Optional
Column("avatar_stamp", Integer), # Optional
Column("event_state", Integer),
Column("event_id", Integer),
Column("sp_bonus_category_id_1", Integer),
Column("sp_bonus_key_value_1", Integer),
Column("sp_bonus_category_id_2", Integer),
Column("sp_bonus_key_value_2", Integer),
Column("last_play_event_id", Integer), # Optional
Column("event_achievement_flag", JSON), # Repeated, Integer
Column("event_achievement_param", JSON), # Repeated, Integer
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_num_tutorial", Integer), # 1?
mysql_charset="utf8mb4",
)
pokemon_data = Table(
"pokken_pokemon_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("char_id", Integer, nullable=False),
Column("illustration_book_no", Integer),
Column("pokemon_exp", Integer),
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_all_num_tutorial", Integer), # ???
Column("battle_num_tutorial", Integer), # 1?
Column("bp_point_atk", Integer),
Column("bp_point_res", Integer),
Column("bp_point_def", Integer),
Column("bp_point_sp", Integer),
UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"),
mysql_charset="utf8mb4",
)
class PokkenProfileData(BaseData):
def touch_profile(self, user_id: int) -> Optional[int]:
sql = select([profile.c.id]).where(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()["id"]
def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id)
conflict = sql.on_conflict_do_update(set_=dict(user=user_id))
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to create pokken profile for user {user_id}!")
return None
return result.lastrowid
def set_profile_name(
self, user_id: int, new_name: str, gender: Union[int, None] = None
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
trainer_name=new_name,
avatar_gender=gender if gender is not None else profile.c.avatar_gender,
)
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile name for user {user_id}!"
)
def put_extra(
self,
user_id: int,
extra_counter: int,
evt_reward_get_flg: int,
total_play_days: int,
awake_num: int,
use_support_ct: int,
beat_num: int,
aid_skill: int,
last_evt: int,
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
extra_counter=extra_counter,
event_reward_get_flag=evt_reward_get_flg,
total_play_days=total_play_days,
awake_num=awake_num,
use_support_num=use_support_ct,
beat_num=beat_num,
aid_skill=aid_skill,
last_play_event_id=last_evt,
)
)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to put extra data for user {user_id}")
def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
tutorial_progress_flag=tutorial_flags,
)
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile tutorial flags for user {user_id}!"
)
def update_profile_achievement_flags(
self, user_id: int, achievement_flags: List
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
achievement_flag=achievement_flags,
)
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile achievement flags for user {user_id}!"
)
def update_profile_event(
self,
user_id: int,
event_state: List,
event_flags: List[int],
event_param: List[int],
last_evt: int = None,
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
event_state=event_state,
event_achievement_flag=event_flags,
event_achievement_param=event_param,
last_play_event_id=last_evt
if last_evt is not None
else profile.c.last_play_event_id,
)
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile event state for user {user_id}!"
)
def add_profile_points(
self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
trainer_rank_point=profile.c.trainer_rank_point + rank_pts,
fight_money=profile.c.fight_money + money,
score_point=profile.c.score_point + score_pts,
grade_max_num=grade_max,
)
)
def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_pokemon(
self,
user_id: int,
pokemon_id: int,
illust_no: int,
atk: int,
res: int,
defe: int,
sp: int,
) -> Optional[int]:
sql = insert(pokemon_data).values(
user=user_id,
char_id=pokemon_id,
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
conflict = sql.on_conflict_do_update(
set_=dict(
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"Failed to insert pokemon ID {pokemon_id} for user {user_id}"
)
return None
return result.lastrowid
def add_pokemon_xp(self, user_id: int, pokemon_id: int, xp: int) -> None:
sql = (
update(pokemon_data)
.where(
and_(
pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id
)
)
.values(pokemon_exp=pokemon_data.c.pokemon_exp + xp)
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}"
)
def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
pass
def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]:
pass
def put_pokemon_battle_result(
self,
user_id: int,
pokemon_id: int,
match_type: PokkenConstants.BATTLE_TYPE,
match_result: PokkenConstants.BATTLE_RESULT,
) -> None:
"""
Records the match stats (type and win/loss) for the pokemon and profile
"""
sql = (
update(pokemon_data)
.where(
and_(
pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id
)
)
.values(
battle_num_tutorial=pokemon_data.c.battle_num_tutorial + 1
if match_type == PokkenConstants.BATTLE_TYPE.TUTORIAL
else pokemon_data.c.battle_num_tutorial,
battle_all_num_tutorial=pokemon_data.c.battle_all_num_tutorial + 1
if match_type == PokkenConstants.BATTLE_TYPE.TUTORIAL
else pokemon_data.c.battle_all_num_tutorial,
battle_num_vs_cpu=pokemon_data.c.battle_num_vs_cpu + 1
if match_type == PokkenConstants.BATTLE_TYPE.AI
else pokemon_data.c.battle_num_vs_cpu,
win_cpu=pokemon_data.c.win_cpu + 1
if match_type == PokkenConstants.BATTLE_TYPE.AI
and match_result == PokkenConstants.BATTLE_RESULT.WIN
else pokemon_data.c.win_cpu,
battle_num_vs_lan=pokemon_data.c.battle_num_vs_lan + 1
if match_type == PokkenConstants.BATTLE_TYPE.LAN
else pokemon_data.c.battle_num_vs_lan,
win_vs_lan=pokemon_data.c.win_vs_lan + 1
if match_type == PokkenConstants.BATTLE_TYPE.LAN
and match_result == PokkenConstants.BATTLE_RESULT.WIN
else pokemon_data.c.win_vs_lan,
battle_num_vs_wan=pokemon_data.c.battle_num_vs_wan + 1
if match_type == PokkenConstants.BATTLE_TYPE.WAN
else pokemon_data.c.battle_num_vs_wan,
win_vs_wan=pokemon_data.c.win_vs_wan + 1
if match_type == PokkenConstants.BATTLE_TYPE.WAN
and match_result == PokkenConstants.BATTLE_RESULT.WIN
else pokemon_data.c.win_vs_wan,
)
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})"
)
def put_stats(
self,
user_id: int,
exkos: int,
wkos: int,
timeout_wins: int,
cool_kos: int,
perfects: int,
continues: int,
) -> None:
"""
Records profile stats
"""
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
ex_ko_num=profile.c.ex_ko_num + exkos,
wko_num=profile.c.wko_num + wkos,
timeup_win_num=profile.c.timeup_win_num + timeout_wins,
cool_ko_num=profile.c.cool_ko_num + cool_kos,
perfect_ko_num=profile.c.perfect_ko_num + perfects,
continue_num=continues,
)
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update stats for user {user_id}")
def update_support_team(
self,
user_id: int,
support_id: int,
support1: int = 4294967295,
support2: int = 4294967295,
) -> None:
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(
support_set_1_1=support1
if support_id == 1
else profile.c.support_set_1_1,
support_set_1_2=support2
if support_id == 1
else profile.c.support_set_1_2,
support_set_2_1=support1
if support_id == 2
else profile.c.support_set_2_1,
support_set_2_2=support2
if support_id == 2
else profile.c.support_set_2_2,
support_set_3_1=support1
if support_id == 3
else profile.c.support_set_3_1,
support_set_3_2=support2
if support_id == 3
else profile.c.support_set_3_2,
)
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"Failed to update support team {support_id} for user {user_id}"
)

View File

@ -1,13 +1,13 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
class PokkenStaticData(BaseData):
pass
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
class PokkenStaticData(BaseData):
pass

View File

@ -10,6 +10,7 @@ from core.config import CoreConfig
from .config import PokkenConfig
from .base import PokkenBase
class PokkenAdmissionProtocol(WebSocketServerProtocol):
def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig):
super().__init__()
@ -18,48 +19,51 @@ class PokkenAdmissionProtocol(WebSocketServerProtocol):
self.logger = logging.getLogger("pokken")
self.base = PokkenBase(cfg, game_cfg)
def onConnect(self, request: ConnectionRequest) -> None:
self.logger.debug(f"Admission: Connection from {request.peer}")
def onClose(self, wasClean: bool, code: int, reason: str) -> None:
self.logger.debug(f"Admission: Connection with {self.transport.getPeer().host} closed {'cleanly ' if wasClean else ''}with code {code} - {reason}")
self.logger.debug(
f"Admission: Connection with {self.transport.getPeer().host} closed {'cleanly ' if wasClean else ''}with code {code} - {reason}"
)
def onMessage(self, payload, isBinary: bool) -> None:
msg: Dict = json.loads(payload)
self.logger.debug(f"Admission: Message from {self.transport.getPeer().host}:{self.transport.getPeer().port} - {msg}")
self.logger.debug(
f"Admission: Message from {self.transport.getPeer().host}:{self.transport.getPeer().port} - {msg}"
)
api = msg.get("api", "noop")
handler = getattr(self.base, f"handle_admission_{api.lower()}")
resp = handler(msg, self.transport.getPeer().host)
if resp is None:
resp = {}
if "type" not in resp:
resp['type'] = "res"
resp["type"] = "res"
if "data" not in resp:
resp['data'] = {}
resp["data"] = {}
if "api" not in resp:
resp['api'] = api
resp["api"] = api
if "result" not in resp:
resp['result'] = 'true'
resp["result"] = "true"
self.logger.debug(f"Websocket response: {resp}")
self.sendMessage(json.dumps(resp).encode(), isBinary)
class PokkenAdmissionFactory(WebSocketServerFactory):
protocol = PokkenAdmissionProtocol
def __init__(
self,
cfg: CoreConfig,
game_cfg: PokkenConfig
) -> None:
def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig) -> None:
self.core_config = cfg
self.game_config = game_cfg
super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.ports.admission}")
super().__init__(
f"ws://{self.game_config.server.hostname}:{self.game_config.ports.admission}"
)
def buildProtocol(self, addr: IAddress) -> Protocol:
p = self.protocol(self.core_config, self.game_config)
p.factory = self

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ class SaoConstants:
VER_SAO = 0
VERSION_NAMES = ("Sword Art Online Arcade")
VERSION_NAMES = "Sword Art Online Arcade"
@classmethod
def game_ver_to_string(cls, ver: int):

View File

@ -10,4 +10,4 @@ class SaoData(Data):
self.item = SaoItemData(cfg, self.session)
self.profile = SaoProfileData(cfg, self.session)
self.static = SaoStaticData(cfg, self.session)
self.static = SaoStaticData(cfg, self.session)

View File

@ -1 +1 @@
from titles.sao.handlers.base import *
from titles.sao.handlers.base import *

File diff suppressed because it is too large Load Diff

View File

@ -97,8 +97,10 @@ class SaoServlet(resource.Resource):
req_url = request.uri.decode()
if req_url == "/matching":
self.logger.info("Matching request")
request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8")
request.responseHeaders.addRawHeader(
b"content-type", b"text/html; charset=utf-8"
)
sao_request = request.content.getvalue().hex()
@ -106,7 +108,9 @@ class SaoServlet(resource.Resource):
if handler is None:
self.logger.info(f"Generic Handler for {req_url} - {sao_request[:4]}")
self.logger.debug(f"Request: {request.content.getvalue().hex()}")
resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1)
resp = SaoNoopResponse(
int.from_bytes(bytes.fromhex(sao_request[:4]), "big") + 1
)
self.logger.debug(f"Response: {resp.make().hex()}")
return resp.make()
@ -114,4 +118,4 @@ class SaoServlet(resource.Resource):
self.logger.debug(f"Request: {request.content.getvalue().hex()}")
resp = handler(sao_request)
self.logger.debug(f"Response: {resp.hex()}")
return resp
return resp

View File

@ -2,6 +2,7 @@ from typing import Optional, Dict, List
from os import walk, path
import urllib
import csv
from contextlib import nullcontext
from read import BaseReader
from core.config import CoreConfig
@ -37,7 +38,8 @@ class SaoReader(BaseReader):
pull_bin_ram = False
if pull_bin_ram:
self.read_csv(f"{self.bin_dir}")
with self.data.static.conn.begin() or nullcontext():
self.read_csv(f"{self.bin_dir}")
def read_csv(self, bin_dir: str) -> None:
self.logger.info(f"Read csv from {bin_dir}")
@ -54,20 +56,16 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added quest {questSceneId} | Name: {name}")
try:
self.data.static.put_quest(
questSceneId,
0,
sortNo,
name,
enabled
questSceneId, 0, sortNo, name, enabled
)
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading HeroLog.csv")
try:
fullPath = bin_dir + "/HeroLog.csv"
@ -84,7 +82,7 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added hero {heroLogId} | Name: {name}")
try:
self.data.static.put_hero(
0,
@ -95,13 +93,13 @@ class SaoReader(BaseReader):
skillTableSubId,
awakeningExp,
flavorText,
enabled
enabled,
)
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading Equipment.csv")
try:
fullPath = bin_dir + "/Equipment.csv"
@ -117,7 +115,7 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added equipment {equipmentId} | Name: {name}")
try:
self.data.static.put_equipment(
0,
@ -127,7 +125,7 @@ class SaoReader(BaseReader):
weaponTypeId,
rarity,
flavorText,
enabled
enabled,
)
except Exception as err:
print(err)
@ -148,22 +146,16 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added item {itemId} | Name: {name}")
try:
self.data.static.put_item(
0,
itemId,
name,
itemTypeId,
rarity,
flavorText,
enabled
0, itemId, name, itemTypeId, rarity, flavorText, enabled
)
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading SupportLog.csv")
try:
fullPath = bin_dir + "/SupportLog.csv"
@ -179,7 +171,7 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added support log {supportLogId} | Name: {name}")
try:
self.data.static.put_support_log(
0,
@ -189,13 +181,13 @@ class SaoReader(BaseReader):
rarity,
salePrice,
skillName,
enabled
enabled,
)
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading Title.csv")
try:
fullPath = bin_dir + "/Title.csv"
@ -210,7 +202,7 @@ class SaoReader(BaseReader):
enabled = True
self.logger.info(f"Added title {titleId} | Name: {displayName}")
if len(titleId) > 5:
try:
self.data.static.put_title(
@ -220,11 +212,13 @@ class SaoReader(BaseReader):
requirement,
rank,
imageFilePath,
enabled
enabled,
)
except Exception as err:
print(err)
elif len(titleId) < 6: # current server code cannot have multiple lengths for the id
elif (
len(titleId) < 6
): # current server code cannot have multiple lengths for the id
continue
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
@ -239,14 +233,13 @@ class SaoReader(BaseReader):
commonRewardId = row["CommonRewardId"]
enabled = True
self.logger.info(f"Added rare drop {questRareDropId} | Reward: {commonRewardId}")
self.logger.info(
f"Added rare drop {questRareDropId} | Reward: {commonRewardId}"
)
try:
self.data.static.put_rare_drop(
0,
questRareDropId,
commonRewardId,
enabled
0, questRareDropId, commonRewardId, enabled
)
except Exception as err:
print(err)

View File

@ -1,3 +1,3 @@
from .profile import SaoProfileData
from .static import SaoStaticData
from .item import SaoItemData
from .profile import SaoProfileData
from .static import SaoStaticData
from .item import SaoItemData

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +1,92 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from ..const import SaoConstants
profile = Table(
"sao_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("user_type", Integer, server_default="1"),
Column("nick_name", String(16), server_default="PLAYER"),
Column("rank_num", Integer, server_default="1"),
Column("rank_exp", Integer, server_default="0"),
Column("own_col", Integer, server_default="0"),
Column("own_vp", Integer, server_default="0"),
Column("own_yui_medal", Integer, server_default="0"),
Column("setting_title_id", Integer, server_default="20005"),
)
class SaoProfileData(BaseData):
def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id)
conflict = sql.on_duplicate_key_update(user=user_id)
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to create SAO profile for user {user_id}!")
return None
return result.lastrowid
def put_profile(self, user_id: int, user_type: int, nick_name: str, rank_num: int, rank_exp: int, own_col: int, own_vp: int, own_yui_medal: int, setting_title_id: int) -> Optional[int]:
sql = insert(profile).values(
user=user_id,
user_type=user_type,
nick_name=nick_name,
rank_num=rank_num,
rank_exp=rank_exp,
own_col=own_col,
own_vp=own_vp,
own_yui_medal=own_yui_medal,
setting_title_id=setting_title_id
)
conflict = sql.on_duplicate_key_update(
rank_num=rank_num,
rank_exp=rank_exp,
own_col=own_col,
own_vp=own_vp,
own_yui_medal=own_yui_medal,
setting_title_id=setting_title_id
)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert profile! user: {user_id}"
)
return None
print(result.lastrowid)
return result.lastrowid
def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
from ..const import SaoConstants
profile = Table(
"sao_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("user_type", Integer, server_default="1"),
Column("nick_name", String(16), server_default="PLAYER"),
Column("rank_num", Integer, server_default="1"),
Column("rank_exp", Integer, server_default="0"),
Column("own_col", Integer, server_default="0"),
Column("own_vp", Integer, server_default="0"),
Column("own_yui_medal", Integer, server_default="0"),
Column("setting_title_id", Integer, server_default="20005"),
)
class SaoProfileData(BaseData):
def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id)
conflict = sql.on_conflict_do_update(set_=dict(user=user_id))
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to create SAO profile for user {user_id}!")
return None
return result.lastrowid
def put_profile(
self,
user_id: int,
user_type: int,
nick_name: str,
rank_num: int,
rank_exp: int,
own_col: int,
own_vp: int,
own_yui_medal: int,
setting_title_id: int,
) -> Optional[int]:
sql = insert(profile).values(
user=user_id,
user_type=user_type,
nick_name=nick_name,
rank_num=rank_num,
rank_exp=rank_exp,
own_col=own_col,
own_vp=own_vp,
own_yui_medal=own_yui_medal,
setting_title_id=setting_title_id,
)
conflict = sql.on_conflict_do_update(
set_=dict(
rank_num=rank_num,
rank_exp=rank_exp,
own_col=own_col,
own_vp=own_vp,
own_yui_medal=own_yui_medal,
setting_title_id=setting_title_id,
)
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert profile! user: {user_id}")
return None
print(result.lastrowid)
return result.lastrowid
def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@ -1,368 +1,413 @@
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
quest = Table(
"sao_static_quest",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("questSceneId", Integer),
Column("sortNo", Integer),
Column("name", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "questSceneId", name="sao_static_quest_uk"
),
mysql_charset="utf8mb4",
)
hero = Table(
"sao_static_hero_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("heroLogId", Integer),
Column("name", String(255)),
Column("nickname", String(255)),
Column("rarity", Integer),
Column("skillTableSubId", Integer),
Column("awakeningExp", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "heroLogId", name="sao_static_hero_list_uk"
),
mysql_charset="utf8mb4",
)
equipment = Table(
"sao_static_equipment_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("equipmentId", Integer),
Column("equipmentType", Integer),
Column("weaponTypeId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "equipmentId", name="sao_static_equipment_list_uk"
),
mysql_charset="utf8mb4",
)
item = Table(
"sao_static_item_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("itemId", Integer),
Column("itemTypeId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "itemId", name="sao_static_item_list_uk"
),
mysql_charset="utf8mb4",
)
support = Table(
"sao_static_support_log_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("supportLogId", Integer),
Column("charaId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("salePrice", Integer),
Column("skillName", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "supportLogId", name="sao_static_support_log_list_uk"
),
mysql_charset="utf8mb4",
)
rare_drop = Table(
"sao_static_rare_drop_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("questRareDropId", Integer),
Column("commonRewardId", Integer),
Column("enabled", Boolean),
UniqueConstraint(
"version", "questRareDropId", "commonRewardId", name="sao_static_rare_drop_list_uk"
),
mysql_charset="utf8mb4",
)
title = Table(
"sao_static_title_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("titleId", Integer),
Column("displayName", String(255)),
Column("requirement", Integer),
Column("rank", Integer),
Column("imageFilePath", String(255)),
Column("enabled", Boolean),
UniqueConstraint(
"version", "titleId", name="sao_static_title_list_uk"
),
mysql_charset="utf8mb4",
)
class SaoStaticData(BaseData):
def put_quest( self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool ) -> Optional[int]:
sql = insert(quest).values(
questSceneId=questSceneId,
version=version,
sortNo=sortNo,
name=name,
tutorial=tutorial,
)
conflict = sql.on_duplicate_key_update(
name=name, questSceneId=questSceneId, version=version
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_hero( self, version: int, heroLogId: int, name: str, nickname: str, rarity: int, skillTableSubId: int, awakeningExp: int, flavorText: str, enabled: bool ) -> Optional[int]:
sql = insert(hero).values(
version=version,
heroLogId=heroLogId,
name=name,
nickname=nickname,
rarity=rarity,
skillTableSubId=skillTableSubId,
awakeningExp=awakeningExp,
flavorText=flavorText,
enabled=enabled
)
conflict = sql.on_duplicate_key_update(
name=name, heroLogId=heroLogId
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_equipment( self, version: int, equipmentId: int, name: str, equipmentType: int, weaponTypeId:int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]:
sql = insert(equipment).values(
version=version,
equipmentId=equipmentId,
name=name,
equipmentType=equipmentType,
weaponTypeId=weaponTypeId,
rarity=rarity,
flavorText=flavorText,
enabled=enabled
)
conflict = sql.on_duplicate_key_update(
name=name, equipmentId=equipmentId
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_item( self, version: int, itemId: int, name: str, itemTypeId: int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]:
sql = insert(item).values(
version=version,
itemId=itemId,
name=name,
itemTypeId=itemTypeId,
rarity=rarity,
flavorText=flavorText,
enabled=enabled
)
conflict = sql.on_duplicate_key_update(
name=name, itemId=itemId
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_support_log( self, version: int, supportLogId: int, charaId: int, name: str, rarity: int, salePrice: int, skillName: str, enabled: bool ) -> Optional[int]:
sql = insert(support).values(
version=version,
supportLogId=supportLogId,
charaId=charaId,
name=name,
rarity=rarity,
salePrice=salePrice,
skillName=skillName,
enabled=enabled
)
conflict = sql.on_duplicate_key_update(
name=name, supportLogId=supportLogId
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_rare_drop( self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool ) -> Optional[int]:
sql = insert(rare_drop).values(
version=version,
questRareDropId=questRareDropId,
commonRewardId=commonRewardId,
enabled=enabled,
)
conflict = sql.on_duplicate_key_update(
questRareDropId=questRareDropId, commonRewardId=commonRewardId, version=version
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]:
sql = insert(title).values(
version=version,
titleId=titleId,
displayName=displayName,
requirement=requirement,
rank=rank,
imageFilePath=imageFilePath,
enabled=enabled
)
conflict = sql.on_duplicate_key_update(
displayName=displayName, titleId=titleId
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_quests_id(self, sortNo: int) -> Optional[Dict]:
sql = quest.select(quest.c.sortNo == sortNo)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by(
quest.c.questSceneId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_hero_id(self, heroLogId: int) -> Optional[Dict]:
sql = hero.select(hero.c.heroLogId == heroLogId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = hero.select(hero.c.version == version and hero.c.enabled == enabled).order_by(
hero.c.heroLogId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_equipment_id(self, equipmentId: int) -> Optional[Dict]:
sql = equipment.select(equipment.c.equipmentId == equipmentId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = equipment.select(equipment.c.version == version and equipment.c.enabled == enabled).order_by(
equipment.c.equipmentId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_item_id(self, itemId: int) -> Optional[Dict]:
sql = item.select(item.c.itemId == itemId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]:
sql = rare_drop.select(rare_drop.c.questRareDropId == questRareDropId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by(
item.c.itemId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_support_log_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = support.select(support.c.version == version and support.c.enabled == enabled).order_by(
support.c.supportLogId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_title_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = title.select(title.c.version == version and title.c.enabled == enabled).order_by(
title.c.titleId.asc()
)
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
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.engine.base import Connection
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.sqlite import insert
from core.data.schema import BaseData, metadata
quest = Table(
"sao_static_quest",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("questSceneId", Integer),
Column("sortNo", Integer),
Column("name", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "questSceneId", name="sao_static_quest_uk"),
mysql_charset="utf8mb4",
)
hero = Table(
"sao_static_hero_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("heroLogId", Integer),
Column("name", String(255)),
Column("nickname", String(255)),
Column("rarity", Integer),
Column("skillTableSubId", Integer),
Column("awakeningExp", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "heroLogId", name="sao_static_hero_list_uk"),
mysql_charset="utf8mb4",
)
equipment = Table(
"sao_static_equipment_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("equipmentId", Integer),
Column("equipmentType", Integer),
Column("weaponTypeId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "equipmentId", name="sao_static_equipment_list_uk"),
mysql_charset="utf8mb4",
)
item = Table(
"sao_static_item_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("itemId", Integer),
Column("itemTypeId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("flavorText", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "itemId", name="sao_static_item_list_uk"),
mysql_charset="utf8mb4",
)
support = Table(
"sao_static_support_log_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("supportLogId", Integer),
Column("charaId", Integer),
Column("name", String(255)),
Column("rarity", Integer),
Column("salePrice", Integer),
Column("skillName", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "supportLogId", name="sao_static_support_log_list_uk"),
mysql_charset="utf8mb4",
)
rare_drop = Table(
"sao_static_rare_drop_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("questRareDropId", Integer),
Column("commonRewardId", Integer),
Column("enabled", Boolean),
UniqueConstraint(
"version",
"questRareDropId",
"commonRewardId",
name="sao_static_rare_drop_list_uk",
),
mysql_charset="utf8mb4",
)
title = Table(
"sao_static_title_list",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer),
Column("titleId", Integer),
Column("displayName", String(255)),
Column("requirement", Integer),
Column("rank", Integer),
Column("imageFilePath", String(255)),
Column("enabled", Boolean),
UniqueConstraint("version", "titleId", name="sao_static_title_list_uk"),
mysql_charset="utf8mb4",
)
class SaoStaticData(BaseData):
def put_quest(
self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool
) -> Optional[int]:
sql = insert(quest).values(
questSceneId=questSceneId,
version=version,
sortNo=sortNo,
name=name,
tutorial=tutorial,
)
conflict = sql.on_conflict_do_update(
set_=dict(name=name, questSceneId=questSceneId, version=version)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_hero(
self,
version: int,
heroLogId: int,
name: str,
nickname: str,
rarity: int,
skillTableSubId: int,
awakeningExp: int,
flavorText: str,
enabled: bool,
) -> Optional[int]:
sql = insert(hero).values(
version=version,
heroLogId=heroLogId,
name=name,
nickname=nickname,
rarity=rarity,
skillTableSubId=skillTableSubId,
awakeningExp=awakeningExp,
flavorText=flavorText,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(set_=dict(name=name, heroLogId=heroLogId))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_equipment(
self,
version: int,
equipmentId: int,
name: str,
equipmentType: int,
weaponTypeId: int,
rarity: int,
flavorText: str,
enabled: bool,
) -> Optional[int]:
sql = insert(equipment).values(
version=version,
equipmentId=equipmentId,
name=name,
equipmentType=equipmentType,
weaponTypeId=weaponTypeId,
rarity=rarity,
flavorText=flavorText,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(
set_=dict(name=name, equipmentId=equipmentId)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_item(
self,
version: int,
itemId: int,
name: str,
itemTypeId: int,
rarity: int,
flavorText: str,
enabled: bool,
) -> Optional[int]:
sql = insert(item).values(
version=version,
itemId=itemId,
name=name,
itemTypeId=itemTypeId,
rarity=rarity,
flavorText=flavorText,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(set_=dict(name=name, itemId=itemId))
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_support_log(
self,
version: int,
supportLogId: int,
charaId: int,
name: str,
rarity: int,
salePrice: int,
skillName: str,
enabled: bool,
) -> Optional[int]:
sql = insert(support).values(
version=version,
supportLogId=supportLogId,
charaId=charaId,
name=name,
rarity=rarity,
salePrice=salePrice,
skillName=skillName,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(
set_=dict(name=name, supportLogId=supportLogId)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_rare_drop(
self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool
) -> Optional[int]:
sql = insert(rare_drop).values(
version=version,
questRareDropId=questRareDropId,
commonRewardId=commonRewardId,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(
set_=dict(
questRareDropId=questRareDropId,
commonRewardId=commonRewardId,
version=version,
)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def put_title(
self,
version: int,
titleId: int,
displayName: str,
requirement: int,
rank: int,
imageFilePath: str,
enabled: bool,
) -> Optional[int]:
sql = insert(title).values(
version=version,
titleId=titleId,
displayName=displayName,
requirement=requirement,
rank=rank,
imageFilePath=imageFilePath,
enabled=enabled,
)
conflict = sql.on_conflict_do_update(
set_=dict(displayName=displayName, titleId=titleId)
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def get_quests_id(self, sortNo: int) -> Optional[Dict]:
sql = quest.select(quest.c.sortNo == sortNo)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = quest.select(
quest.c.version == version and quest.c.enabled == enabled
).order_by(quest.c.questSceneId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_hero_id(self, heroLogId: int) -> Optional[Dict]:
sql = hero.select(hero.c.heroLogId == heroLogId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = hero.select(
hero.c.version == version and hero.c.enabled == enabled
).order_by(hero.c.heroLogId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_equipment_id(self, equipmentId: int) -> Optional[Dict]:
sql = equipment.select(equipment.c.equipmentId == equipmentId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = equipment.select(
equipment.c.version == version and equipment.c.enabled == enabled
).order_by(equipment.c.equipmentId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_item_id(self, itemId: int) -> Optional[Dict]:
sql = item.select(item.c.itemId == itemId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]:
sql = rare_drop.select(rare_drop.c.questRareDropId == questRareDropId)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = item.select(
item.c.version == version and item.c.enabled == enabled
).order_by(item.c.itemId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_support_log_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = support.select(
support.c.version == version and support.c.enabled == enabled
).order_by(support.c.supportLogId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]
def get_title_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]:
sql = title.select(
title.c.version == version and title.c.enabled == enabled
).order_by(title.c.titleId.asc())
result = self.execute(sql)
if result is None:
return None
return [list[2] for list in result.fetchall()]

View File

@ -7,6 +7,7 @@ from core.config import CoreConfig
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.database import WaccaData
from contextlib import nullcontext
from titles.wacca.handlers import *
from core.const import AllnetCountryCode
@ -237,42 +238,43 @@ class WaccaBase:
return BaseResponse().make()
# Insert starting items
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005)
with self.data.item.conn.begin() or nullcontext():
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005
) # Added lily
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005
) # Added lily
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001
)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001
) # Added lily
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001
) # Added lily
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000
) # Added reverse
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001
) # Added reverse
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000
) # Added reverse
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001
) # Added reverse
return UserStatusCreateResponseV2(profileId, req.username).make()
@ -500,83 +502,89 @@ class WaccaBase:
user_id, self.version, req.stageId
)
if old_stage is None:
self.data.score.put_stageup(
user_id,
self.version,
req.stageId,
req.clearType.value,
req.numSongsCleared,
req.songScores[0],
req.songScores[1],
req.songScores[2],
)
else:
# We only care about total score for best of, even if one score happens to be lower (I think)
if total_score > (
old_stage["song1_score"]
+ old_stage["song2_score"]
+ old_stage["song3_score"]
):
best_score1 = req.songScores[0]
best_score2 = req.songScores[1]
best_score3 = req.songScores[2]
else:
best_score1 = old_stage["song1_score"]
best_score2 = old_stage["song2_score"]
best_score3 = old_stage["song3_score"]
self.data.score.put_stageup(
user_id,
self.version,
req.stageId,
req.clearType.value,
req.numSongsCleared,
best_score1,
best_score2,
best_score3,
)
if (
req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0
): # For some reason, special stages send dan level 1001...
if req.stageLevel > profile["dan_level"] or (
req.stageLevel == profile["dan_level"]
and req.clearType.value > profile["dan_type"]
):
self.data.profile.update_profile_dan(
req.profileId, req.stageLevel, req.clearType.value
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
if old_stage is None:
self.data.score.put_stageup(
user_id,
self.version,
req.stageId,
req.clearType.value,
req.numSongsCleared,
req.songScores[0],
req.songScores[1],
req.songScores[2],
)
self.util_put_items(req.profileId, user_id, req.itemsObtained)
else:
# We only care about total score for best of, even if one score happens to be lower (I think)
if total_score > (
old_stage["song1_score"]
+ old_stage["song2_score"]
+ old_stage["song3_score"]
):
best_score1 = req.songScores[0]
best_score2 = req.songScores[1]
best_score3 = req.songScores[2]
else:
best_score1 = old_stage["song1_score"]
best_score2 = old_stage["song2_score"]
best_score3 = old_stage["song3_score"]
# user/status/update isn't called after stageup so we have to do some things now
current_icon = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_icon_id"]
)
current_nav = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_nav_id"]
)
self.data.score.put_stageup(
user_id,
self.version,
req.stageId,
req.clearType.value,
req.numSongsCleared,
best_score1,
best_score2,
best_score3,
)
if current_icon is None:
current_icon = self.OPTIONS_DEFAULTS["set_icon_id"]
else:
current_icon = current_icon["value"]
if current_nav is None:
current_nav = self.OPTIONS_DEFAULTS["set_nav_id"]
else:
current_nav = current_nav["value"]
if (
req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0
): # For some reason, special stages send dan level 1001...
if req.stageLevel > profile["dan_level"] or (
req.stageLevel == profile["dan_level"]
and req.clearType.value > profile["dan_type"]
):
self.data.profile.update_profile_dan(
req.profileId, req.stageLevel, req.clearType.value
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav
)
self.data.profile.update_profile_playtype(
req.profileId, 4, data["appVersion"][:7]
)
self.util_put_items(req.profileId, user_id, req.itemsObtained)
# user/status/update isn't called after stageup so we have to do some things now
current_icon = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_icon_id"]
)
current_nav = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_nav_id"]
)
if current_icon is None:
current_icon = self.OPTIONS_DEFAULTS["set_icon_id"]
else:
current_icon = current_icon["value"]
if current_nav is None:
current_nav = self.OPTIONS_DEFAULTS["set_nav_id"]
else:
current_nav = current_nav["value"]
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav
)
self.data.profile.update_profile_playtype(
req.profileId, 4, data["appVersion"][:7]
)
return BaseResponse().make()
def handle_user_sugoroku_update_request(self, data: Dict) -> Dict:
@ -598,17 +606,22 @@ class WaccaBase:
)
return resp.make()
self.util_put_items(req.profileId, user_id, req.itemsObtainted)
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
self.util_put_items(req.profileId, user_id, req.itemsObtainted)
self.data.profile.update_gate(
user_id,
req.gateId,
req.page,
req.progress,
req.loops,
mission_flg,
req.totalPts,
)
self.data.profile.update_gate(
user_id,
req.gateId,
req.page,
req.progress,
req.loops,
mission_flg,
req.totalPts,
)
return resp.make()
def handle_user_info_getMyroom_request(self, data: Dict) -> Dict:
@ -627,31 +640,37 @@ class WaccaBase:
new_tickets: List[TicketItem] = []
for ticket in tickets:
new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999))
new_tickets.append(
TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)
)
for item in req.itemsUsed:
if (
item.itemType == WaccaConstants.ITEM_TYPES["wp"]
and not self.game_config.mods.infinite_wp
):
if current_wp >= item.quantity:
current_wp -= item.quantity
self.data.profile.spend_wp(req.profileId, item.quantity)
else:
return BaseResponse().make()
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
for item in req.itemsUsed:
if (
item.itemType == WaccaConstants.ITEM_TYPES["wp"]
and not self.game_config.mods.infinite_wp
):
if current_wp >= item.quantity:
current_wp -= item.quantity
self.data.profile.spend_wp(req.profileId, item.quantity)
else:
return BaseResponse().make()
elif (
item.itemType == WaccaConstants.ITEM_TYPES["ticket"]
and not self.game_config.mods.infinite_tickets
):
for x in range(len(new_tickets)):
if new_tickets[x].ticketId == item.itemId:
self.logger.debug(
f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}"
)
self.data.item.spend_ticket(new_tickets[x].userTicketId)
new_tickets.pop(x)
break
elif (
item.itemType == WaccaConstants.ITEM_TYPES["ticket"]
and not self.game_config.mods.infinite_tickets
):
for x in range(len(new_tickets)):
if new_tickets[x].ticketId == item.itemId:
self.logger.debug(
f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}"
)
self.data.item.spend_ticket(new_tickets[x].userTicketId)
new_tickets.pop(x)
break
# wp, ticket info
if req.difficulty > WaccaConstants.Difficulty.HARD.value:
@ -715,118 +734,124 @@ class WaccaBase:
return resp.make()
user_id = profile["user"]
self.util_put_items(req.profileId, user_id, req.itemsObtained)
playlog_clear_status = (
req.songDetail.flagCleared
+ req.songDetail.flagMissless
+ req.songDetail.flagFullcombo
+ req.songDetail.flagAllMarvelous
)
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
self.util_put_items(req.profileId, user_id, req.itemsObtained)
self.data.score.put_playlog(
user_id,
req.songDetail.songId,
req.songDetail.difficulty,
req.songDetail.score,
playlog_clear_status,
req.songDetail.grade.value,
req.songDetail.maxCombo,
req.songDetail.judgements.marvCt,
req.songDetail.judgements.greatCt,
req.songDetail.judgements.goodCt,
req.songDetail.judgements.missCt,
req.songDetail.fastCt,
req.songDetail.slowCt,
self.season,
)
playlog_clear_status = (
req.songDetail.flagCleared
+ req.songDetail.flagMissless
+ req.songDetail.flagFullcombo
+ req.songDetail.flagAllMarvelous
)
old_score = self.data.score.get_best_score(
user_id, req.songDetail.songId, req.songDetail.difficulty
)
if not old_score:
grades = [0] * 13
clears = [0] * 5
clears[0] = 1
clears[1] = 1 if req.songDetail.flagCleared else 0
clears[2] = 1 if req.songDetail.flagMissless else 0
clears[3] = 1 if req.songDetail.flagFullcombo else 0
clears[4] = 1 if req.songDetail.flagAllMarvelous else 0
grades[req.songDetail.grade.value - 1] = 1
self.data.score.put_best_score(
self.data.score.put_playlog(
user_id,
req.songDetail.songId,
req.songDetail.difficulty,
req.songDetail.score,
clears,
grades,
playlog_clear_status,
req.songDetail.grade.value,
req.songDetail.maxCombo,
req.songDetail.judgements.marvCt,
req.songDetail.judgements.greatCt,
req.songDetail.judgements.goodCt,
req.songDetail.judgements.missCt,
req.songDetail.fastCt,
req.songDetail.slowCt,
self.season,
)
resp.songDetail.score = req.songDetail.score
resp.songDetail.lowestMissCount = req.songDetail.judgements.missCt
else:
grades = [
old_score["grade_d_ct"],
old_score["grade_c_ct"],
old_score["grade_b_ct"],
old_score["grade_a_ct"],
old_score["grade_aa_ct"],
old_score["grade_aaa_ct"],
old_score["grade_s_ct"],
old_score["grade_ss_ct"],
old_score["grade_sss_ct"],
old_score["grade_master_ct"],
old_score["grade_sp_ct"],
old_score["grade_ssp_ct"],
old_score["grade_sssp_ct"],
]
clears = [
old_score["play_ct"],
old_score["clear_ct"],
old_score["missless_ct"],
old_score["fullcombo_ct"],
old_score["allmarv_ct"],
]
clears[0] += 1
clears[1] += 1 if req.songDetail.flagCleared else 0
clears[2] += 1 if req.songDetail.flagMissless else 0
clears[3] += 1 if req.songDetail.flagFullcombo else 0
clears[4] += 1 if req.songDetail.flagAllMarvelous else 0
grades[req.songDetail.grade.value - 1] += 1
best_score = max(req.songDetail.score, old_score["score"])
best_max_combo = max(req.songDetail.maxCombo, old_score["best_combo"])
lowest_miss_ct = min(
req.songDetail.judgements.missCt, old_score["lowest_miss_ct"]
)
best_rating = max(
self.util_calc_song_rating(req.songDetail.score, req.songDetail.level),
old_score["rating"],
old_score = self.data.score.get_best_score(
user_id, req.songDetail.songId, req.songDetail.difficulty
)
self.data.score.put_best_score(
user_id,
req.songDetail.songId,
req.songDetail.difficulty,
best_score,
clears,
grades,
best_max_combo,
lowest_miss_ct,
)
if not old_score:
grades = [0] * 13
clears = [0] * 5
resp.songDetail.score = best_score
resp.songDetail.lowestMissCount = lowest_miss_ct
resp.songDetail.rating = best_rating
clears[0] = 1
clears[1] = 1 if req.songDetail.flagCleared else 0
clears[2] = 1 if req.songDetail.flagMissless else 0
clears[3] = 1 if req.songDetail.flagFullcombo else 0
clears[4] = 1 if req.songDetail.flagAllMarvelous else 0
grades[req.songDetail.grade.value - 1] = 1
self.data.score.put_best_score(
user_id,
req.songDetail.songId,
req.songDetail.difficulty,
req.songDetail.score,
clears,
grades,
req.songDetail.maxCombo,
req.songDetail.judgements.missCt,
)
resp.songDetail.score = req.songDetail.score
resp.songDetail.lowestMissCount = req.songDetail.judgements.missCt
else:
grades = [
old_score["grade_d_ct"],
old_score["grade_c_ct"],
old_score["grade_b_ct"],
old_score["grade_a_ct"],
old_score["grade_aa_ct"],
old_score["grade_aaa_ct"],
old_score["grade_s_ct"],
old_score["grade_ss_ct"],
old_score["grade_sss_ct"],
old_score["grade_master_ct"],
old_score["grade_sp_ct"],
old_score["grade_ssp_ct"],
old_score["grade_sssp_ct"],
]
clears = [
old_score["play_ct"],
old_score["clear_ct"],
old_score["missless_ct"],
old_score["fullcombo_ct"],
old_score["allmarv_ct"],
]
clears[0] += 1
clears[1] += 1 if req.songDetail.flagCleared else 0
clears[2] += 1 if req.songDetail.flagMissless else 0
clears[3] += 1 if req.songDetail.flagFullcombo else 0
clears[4] += 1 if req.songDetail.flagAllMarvelous else 0
grades[req.songDetail.grade.value - 1] += 1
best_score = max(req.songDetail.score, old_score["score"])
best_max_combo = max(req.songDetail.maxCombo, old_score["best_combo"])
lowest_miss_ct = min(
req.songDetail.judgements.missCt, old_score["lowest_miss_ct"]
)
best_rating = max(
self.util_calc_song_rating(req.songDetail.score, req.songDetail.level),
old_score["rating"],
)
self.data.score.put_best_score(
user_id,
req.songDetail.songId,
req.songDetail.difficulty,
best_score,
clears,
grades,
best_max_combo,
lowest_miss_ct,
)
resp.songDetail.score = best_score
resp.songDetail.lowestMissCount = lowest_miss_ct
resp.songDetail.rating = best_rating
resp.songDetail.clearCounts = SongDetailClearCounts(counts=clears)
resp.songDetail.clearCountsSeason = SongDetailClearCounts(counts=clears)
@ -858,13 +883,18 @@ class WaccaBase:
if profile is None:
return BaseResponse().make()
if len(req.itemsObtained) > 0:
self.util_put_items(req.profileId, profile["user"], req.itemsObtained)
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
if len(req.itemsObtained) > 0:
self.util_put_items(req.profileId, profile["user"], req.itemsObtained)
self.data.profile.update_bingo(
profile["user"], req.bingoDetail.pageNumber, page_status
)
self.data.profile.update_tutorial_flags(req.profileId, req.params[3])
self.data.profile.update_bingo(
profile["user"], req.bingoDetail.pageNumber, page_status
)
self.data.profile.update_tutorial_flags(req.profileId, req.params[3])
return BaseResponse().make()
@ -891,7 +921,12 @@ class WaccaBase:
f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}"
)
self.util_put_items(req.profileId, user_id, [req.itemObtained])
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
self.util_put_items(req.profileId, user_id, [req.itemObtained])
if self.game_config.mods.infinite_tickets:
for x in range(5):
@ -949,33 +984,38 @@ class WaccaBase:
)
return BaseResponse().make()
self.util_put_items(req.profileId, user_id, req.itemsRecieved)
self.data.profile.update_profile_playtype(
req.profileId, req.playType.value, data["appVersion"][:7]
)
with (
self.data.profile.conn.begin() or nullcontext(),
self.data.score.conn.begin() or nullcontext(),
self.data.item.conn.begin() or nullcontext(),
):
self.util_put_items(req.profileId, user_id, req.itemsRecieved)
self.data.profile.update_profile_playtype(
req.profileId, req.playType.value, data["appVersion"][:7]
)
current_icon = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_icon_id"]
)
current_nav = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_nav_id"]
)
current_icon = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_icon_id"]
)
current_nav = self.data.profile.get_options(
user_id, WaccaConstants.OPTIONS["set_nav_id"]
)
if current_icon is None:
current_icon = self.OPTIONS_DEFAULTS["set_icon_id"]
else:
current_icon = current_icon["value"]
if current_nav is None:
current_nav = self.OPTIONS_DEFAULTS["set_nav_id"]
else:
current_nav = current_nav["value"]
if current_icon is None:
current_icon = self.OPTIONS_DEFAULTS["set_icon_id"]
else:
current_icon = current_icon["value"]
if current_nav is None:
current_nav = self.OPTIONS_DEFAULTS["set_nav_id"]
else:
current_nav = current_nav["value"]
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon
)
self.data.item.put_item(
user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav
)
return BaseResponse().make()
def handle_user_info_update_request(self, data: Dict) -> Dict:

View File

@ -30,9 +30,9 @@ class WaccaFrontend(FE_Base):
)
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh)
sesh=vars(usr_sesh),
).encode("utf-16")

View File

@ -93,7 +93,9 @@ class UserMusicUnlockRequest(BaseRequest):
class UserMusicUnlockResponse(BaseResponse):
def __init__(self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []) -> None:
def __init__(
self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []
) -> None:
super().__init__()
self.wp = current_wp
self.tickets = tickets_remaining

View File

@ -1,6 +1,7 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import json
from contextlib import nullcontext
from core.config import CoreConfig
from titles.wacca.lily import WaccaLily
@ -40,30 +41,31 @@ class WaccaLilyR(WaccaLily):
req = UserStatusCreateRequest(data)
resp = super().handle_user_status_create_request(data)
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061
) # Added lily r
with self.data.item.conn.begin() or nullcontext():
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060
) # Added lily r
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061
) # Added lily r
return resp

Some files were not shown because too many files have changed in this diff Show More