2023-02-17 06:02:21 +00:00
|
|
|
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
|
2023-10-20 03:29:05 +00:00
|
|
|
from sqlalchemy.sql.expression import exists
|
2023-02-17 06:02:21 +00:00
|
|
|
from core.data.schema import BaseData, metadata
|
2024-09-16 01:20:45 +00:00
|
|
|
from ..config import ChuniConfig
|
2023-02-17 06:02:21 +00:00
|
|
|
|
|
|
|
course = Table(
|
|
|
|
"chuni_score_course",
|
|
|
|
metadata,
|
|
|
|
Column("id", Integer, primary_key=True, nullable=False),
|
2023-03-09 16:38:58 +00:00
|
|
|
Column(
|
|
|
|
"user",
|
|
|
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
|
|
|
nullable=False,
|
|
|
|
),
|
2023-02-17 06:02:21 +00:00
|
|
|
Column("courseId", Integer),
|
|
|
|
Column("classId", Integer),
|
|
|
|
Column("playCount", Integer),
|
|
|
|
Column("scoreMax", Integer),
|
|
|
|
Column("isFullCombo", Boolean),
|
|
|
|
Column("isAllJustice", Boolean),
|
2023-12-13 06:04:21 +00:00
|
|
|
Column("isSuccess", Integer),
|
2023-02-17 06:02:21 +00:00
|
|
|
Column("scoreRank", Integer),
|
|
|
|
Column("eventId", Integer),
|
|
|
|
Column("lastPlayDate", String(25)),
|
|
|
|
Column("param1", Integer),
|
|
|
|
Column("param2", Integer),
|
|
|
|
Column("param3", Integer),
|
|
|
|
Column("param4", Integer),
|
2023-12-13 06:04:21 +00:00
|
|
|
Column("isClear", Integer),
|
2023-03-01 21:08:36 +00:00
|
|
|
Column("theoryCount", Integer),
|
|
|
|
Column("orderId", Integer),
|
|
|
|
Column("playerRating", Integer),
|
2023-02-17 06:02:21 +00:00
|
|
|
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
|
2023-03-09 16:38:58 +00:00
|
|
|
mysql_charset="utf8mb4",
|
2023-02-17 06:02:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
best_score = Table(
|
|
|
|
"chuni_score_best",
|
|
|
|
metadata,
|
|
|
|
Column("id", Integer, primary_key=True, nullable=False),
|
2023-03-09 16:38:58 +00:00
|
|
|
Column(
|
|
|
|
"user",
|
|
|
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
|
|
|
nullable=False,
|
|
|
|
),
|
2023-02-17 06:02:21 +00:00
|
|
|
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),
|
2023-12-13 06:04:21 +00:00
|
|
|
Column("isSuccess", Integer),
|
2023-02-17 06:02:21 +00:00
|
|
|
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"),
|
2023-03-09 16:38:58 +00:00
|
|
|
mysql_charset="utf8mb4",
|
2023-02-17 06:02:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
playlog = Table(
|
|
|
|
"chuni_score_playlog",
|
|
|
|
metadata,
|
|
|
|
Column("id", Integer, primary_key=True, nullable=False),
|
2023-03-09 16:38:58 +00:00
|
|
|
Column(
|
|
|
|
"user",
|
|
|
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
|
|
|
nullable=False,
|
|
|
|
),
|
2023-02-17 06:02:21 +00:00
|
|
|
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),
|
2023-12-13 06:04:21 +00:00
|
|
|
Column("isClear", Integer),
|
2023-02-17 06:02:21 +00:00
|
|
|
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),
|
2023-05-10 19:32:35 +00:00
|
|
|
Column("regionId", Integer),
|
|
|
|
Column("machineType", Integer),
|
2023-12-13 06:04:21 +00:00
|
|
|
Column("ticketId", Integer),
|
2023-05-10 19:32:35 +00:00
|
|
|
mysql_charset="utf8mb4"
|
2023-02-17 06:02:21 +00:00
|
|
|
)
|
|
|
|
|
2024-09-16 01:20:45 +00:00
|
|
|
class ChuniRomVersion():
|
|
|
|
"""
|
|
|
|
Class used to easily compare rom version strings and map back to the internal integer version.
|
|
|
|
Used with methods that touch the playlog table.
|
|
|
|
"""
|
|
|
|
Versions = {}
|
|
|
|
def init_versions(cfg: ChuniConfig):
|
|
|
|
if len(ChuniRomVersion.Versions) > 0:
|
|
|
|
# dont bother with reinit
|
|
|
|
return
|
|
|
|
|
|
|
|
# Build up a easily comparible list of versions. Used when deriving romVersion from the playlog
|
|
|
|
all_versions = {
|
|
|
|
10: ChuniRomVersion("1.50.0"),
|
|
|
|
9: ChuniRomVersion("1.45.0"),
|
|
|
|
8: ChuniRomVersion("1.40.0"),
|
|
|
|
7: ChuniRomVersion("1.35.0"),
|
|
|
|
6: ChuniRomVersion("1.30.0"),
|
|
|
|
5: ChuniRomVersion("1.25.0"),
|
|
|
|
4: ChuniRomVersion("1.20.0"),
|
|
|
|
3: ChuniRomVersion("1.15.0"),
|
|
|
|
2: ChuniRomVersion("1.10.0"),
|
|
|
|
1: ChuniRomVersion("1.05.0"),
|
|
|
|
0: ChuniRomVersion("1.00.0")
|
|
|
|
}
|
|
|
|
|
|
|
|
# add the versions from the config
|
|
|
|
for ver in range(11,999):
|
|
|
|
cfg_ver = cfg.version.version(ver)
|
|
|
|
if cfg_ver:
|
|
|
|
all_versions[ver] = ChuniRomVersion(cfg_ver["rom"])
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
# sort it by version number for easy iteration
|
|
|
|
ChuniRomVersion.Versions = dict(sorted(all_versions.items()))
|
|
|
|
|
|
|
|
def __init__(self, rom_version: str) -> None:
|
|
|
|
(major, minor, maint) = rom_version.split('.')
|
|
|
|
self.major = int(major)
|
|
|
|
self.minor = int(minor)
|
|
|
|
self.maint = int(maint)
|
|
|
|
self.version = rom_version
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.version
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return (self.major == other.major and
|
|
|
|
self.minor == other.minor and
|
|
|
|
self.maint == other.maint)
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
return (self.major < other.major) or \
|
|
|
|
(self.major == other.major and self.minor < other.minor) or \
|
|
|
|
(self.major == other.major and self.minor == other.minor and self.maint < other.maint)
|
|
|
|
|
|
|
|
def __gt__(self, other):
|
|
|
|
return (self.major > other.major) or \
|
|
|
|
(self.major == other.major and self.minor > other.minor) or \
|
|
|
|
(self.major == other.major and self.minor == other.minor and self.maint > other.maint)
|
|
|
|
|
|
|
|
def get_int_version(self):
|
|
|
|
"""
|
|
|
|
Used when displaying the playlog to walk backwards from the recorded romVersion to our internal version number.
|
|
|
|
This is effectively a workaround to avoid recording our internal version number along with the romVersion in the db at insert time.
|
|
|
|
"""
|
|
|
|
for ver,rom in ChuniRomVersion.Versions.items():
|
|
|
|
# if the version matches exactly, great!
|
|
|
|
if self == rom:
|
|
|
|
return ver
|
|
|
|
|
|
|
|
# If this isnt the last version, use the next as an upper bound
|
|
|
|
if ver + 1 < len(ChuniRomVersion.Versions):
|
|
|
|
if self > rom and self < ChuniRomVersion.Versions[ver + 1]:
|
|
|
|
# this version fits in the middle! It must be a revision of the version
|
|
|
|
# e.g. 2.15.00 vs 2.16.00
|
|
|
|
return ver
|
|
|
|
else:
|
|
|
|
# this is the last version in the list.
|
|
|
|
# If its greate than this one and still the same major, this call it a match
|
|
|
|
if self.major == rom.major and self > rom:
|
|
|
|
return ver
|
|
|
|
|
|
|
|
# Only way we get here is if it was a version that started with "0." which is def invalid
|
|
|
|
return -1
|
2023-03-09 16:38:58 +00:00
|
|
|
|
2023-02-17 06:02:21 +00:00
|
|
|
class ChuniScoreData(BaseData):
|
2024-01-09 19:42:17 +00:00
|
|
|
async def get_courses(self, aime_id: int) -> Optional[Row]:
|
2023-02-17 06:02:21 +00:00
|
|
|
sql = select(course).where(course.c.user == aime_id)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(sql)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.fetchall()
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
|
2023-02-17 06:02:21 +00:00
|
|
|
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)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(conflict)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.lastrowid
|
2023-03-09 16:38:58 +00:00
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def get_scores(self, aime_id: int) -> Optional[Row]:
|
2023-02-17 06:02:21 +00:00
|
|
|
sql = select(best_score).where(best_score.c.user == aime_id)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(sql)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.fetchall()
|
2023-03-09 16:38:58 +00:00
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
|
2023-02-17 06:02:21 +00:00
|
|
|
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)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(conflict)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.lastrowid
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def get_playlogs(self, aime_id: int) -> Optional[Row]:
|
2023-02-17 06:02:21 +00:00
|
|
|
sql = select(playlog).where(playlog.c.user == aime_id)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(sql)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.fetchall()
|
2023-03-09 16:38:58 +00:00
|
|
|
|
2024-09-16 01:20:45 +00:00
|
|
|
async def get_playlog_rom_versions_by_int_version(self, version: int, aime_id: int = -1) -> []:
|
|
|
|
# Get a set of all romVersion values present
|
|
|
|
sql = select([playlog.c.romVersion])
|
|
|
|
if aime_id != -1:
|
|
|
|
# limit results to a specific user
|
|
|
|
sql = sql.where(playlog.c.user == aime_id)
|
|
|
|
sql = sql.distinct()
|
2024-04-14 17:35:27 +00:00
|
|
|
|
|
|
|
result = await self.execute(sql)
|
|
|
|
if result is None:
|
2024-09-16 01:20:45 +00:00
|
|
|
return []
|
|
|
|
record_versions = result.fetchall()
|
|
|
|
|
|
|
|
# for each romVersion recorded, check if it maps back the current version we are operating on
|
|
|
|
matching_rom_versions = []
|
|
|
|
for v in record_versions:
|
|
|
|
if ChuniRomVersion(v[0]).get_int_version() == version:
|
|
|
|
matching_rom_versions += [v[0]]
|
|
|
|
|
|
|
|
self.logger.debug(f"romVersions {matching_rom_versions} map to version {version}")
|
|
|
|
return matching_rom_versions
|
|
|
|
|
|
|
|
async def get_playlogs_limited(self, aime_id: int, version: int, index: int, count: int) -> Optional[Row]:
|
|
|
|
# Get a list of all the recorded romVersions in the playlog
|
|
|
|
# for this user that map to the given version.
|
|
|
|
rom_versions = await self.get_playlog_rom_versions_by_int_version(version, aime_id)
|
|
|
|
|
|
|
|
# Query results that have the matching romVersions
|
|
|
|
sql = select(playlog).where((playlog.c.user == aime_id) & (playlog.c.romVersion.in_(rom_versions))).order_by(playlog.c.id.desc()).limit(count).offset(index * count)
|
|
|
|
|
|
|
|
result = await self.execute(sql)
|
|
|
|
if result is None:
|
|
|
|
self.logger.info(f" aime_id {aime_id} has no playlog for version {version}")
|
2024-04-14 17:35:27 +00:00
|
|
|
return None
|
|
|
|
return result.fetchall()
|
|
|
|
|
2024-09-16 01:20:45 +00:00
|
|
|
async def get_user_playlogs_count(self, aime_id: int, version: int) -> Optional[Row]:
|
|
|
|
# Get a list of all the recorded romVersions in the playlog
|
|
|
|
# for this user that map to the given version.
|
|
|
|
rom_versions = await self.get_playlog_rom_versions_by_int_version(version, aime_id)
|
|
|
|
|
|
|
|
# Query results that have the matching romVersions
|
|
|
|
sql = select(func.count()).where((playlog.c.user == aime_id) & (playlog.c.romVersion.in_(rom_versions)))
|
|
|
|
|
2024-04-14 17:35:27 +00:00
|
|
|
result = await self.execute(sql)
|
|
|
|
if result is None:
|
2024-09-16 01:20:45 +00:00
|
|
|
self.logger.info(f" aime_id {aime_id} has no playlog for version {version}")
|
|
|
|
return 0
|
2024-04-14 17:35:27 +00:00
|
|
|
return result.scalar()
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
|
2023-02-17 06:02:21 +00:00
|
|
|
playlog_data["user"] = aime_id
|
|
|
|
playlog_data = self.fix_bools(playlog_data)
|
2024-09-16 01:20:45 +00:00
|
|
|
# If the romVersion is not in the data (Version 10 and earlier), look it up from our internal mapping
|
2023-10-20 03:29:05 +00:00
|
|
|
if "romVersion" not in playlog_data:
|
2024-09-16 01:20:45 +00:00
|
|
|
playlog_data["romVersion"] = ChuniRomVersion.Versions[version]
|
2023-02-17 06:02:21 +00:00
|
|
|
|
|
|
|
sql = insert(playlog).values(**playlog_data)
|
|
|
|
conflict = sql.on_duplicate_key_update(**playlog_data)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(conflict)
|
2023-03-09 16:38:58 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-02-17 06:02:21 +00:00
|
|
|
return result.lastrowid
|
2023-07-11 09:13:19 +00:00
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def get_rankings(self, version: int) -> Optional[List[Dict]]:
|
2024-09-16 01:20:45 +00:00
|
|
|
# Get a list of all the recorded romVersions in the playlog for the given version
|
|
|
|
rom_versions = await self.get_playlog_rom_versions_by_int_version(version)
|
|
|
|
|
|
|
|
# Query results that have the matching romVersions
|
|
|
|
sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.in_(rom_versions))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(sql)
|
2023-10-20 03:29:05 +00:00
|
|
|
|
|
|
|
if result is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
rows = result.fetchall()
|
|
|
|
return [dict(row) for row in rows]
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
|
2023-10-20 03:29:05 +00:00
|
|
|
sql = select(best_score).where(best_score.c.user == rival_id)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.execute(sql)
|
2023-07-11 09:13:19 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2023-10-16 13:09:11 +00:00
|
|
|
return result.fetchall()
|