2024-03-30 03:07:21 +01:00
from sqlalchemy import Table , Column , UniqueConstraint
from sqlalchemy . exc import IntegrityError
from sqlalchemy . types import String , Integer , JSON
from sqlalchemy . dialects . mysql import BIGINT as BigInteger
2019-12-08 22:43:49 +01:00
from typing import Optional , Dict , List , Tuple , Any
2021-08-19 21:21:22 +02:00
from bemani . common import GameConstants , Time
2019-12-08 22:43:49 +01:00
from bemani . data . exceptions import ScoreSaveException
from bemani . data . mysql . base import BaseData , metadata
from bemani . data . types import Score , Attempt , Song , UserID
"""
Table for storing a score for a particular game . This is keyed by userid and
musicid , as a user can only have one score for a particular song / chart combo .
This has a JSON blob for any data the game wishes to store , such as points , medals ,
ghost , etc .
Note that this is NOT keyed by game song id and chart , but by an internal musicid
managed by the music table . This is so we can support keeping the same score across
multiple games , even if the game changes the ID it refers to the song by .
"""
2021-05-31 20:07:03 +02:00
score = Table (
2022-10-15 20:56:30 +02:00
" score " ,
2019-12-08 22:43:49 +01:00
metadata ,
2022-10-15 20:56:30 +02:00
Column ( " id " , Integer , nullable = False , primary_key = True ) ,
Column ( " userid " , BigInteger ( unsigned = True ) , nullable = False ) ,
Column ( " musicid " , Integer , nullable = False , index = True ) ,
Column ( " points " , Integer , nullable = False , index = True ) ,
Column ( " timestamp " , Integer , nullable = False , index = True ) ,
Column ( " update " , Integer , nullable = False , index = True ) ,
Column ( " lid " , Integer , nullable = False , index = True ) ,
Column ( " data " , JSON , nullable = False ) ,
UniqueConstraint ( " userid " , " musicid " , name = " userid_musicid " ) ,
mysql_charset = " utf8mb4 " ,
2019-12-08 22:43:49 +01:00
)
"""
Table for storing score history for a particular game . Every entry that is stored
or updated in score will be written into this table as well , for looking up history
over time .
"""
2021-05-31 20:07:03 +02:00
score_history = Table (
2022-10-15 20:56:30 +02:00
" score_history " ,
2019-12-08 22:43:49 +01:00
metadata ,
2022-10-15 20:56:30 +02:00
Column ( " id " , Integer , nullable = False , primary_key = True ) ,
Column ( " userid " , BigInteger ( unsigned = True ) , nullable = False ) ,
Column ( " musicid " , Integer , nullable = False , index = True ) ,
Column ( " points " , Integer , nullable = False ) ,
Column ( " timestamp " , Integer , nullable = False , index = True ) ,
Column ( " lid " , Integer , nullable = False , index = True ) ,
Column ( " new_record " , Integer , nullable = False ) ,
Column ( " data " , JSON , nullable = False ) ,
UniqueConstraint ( " userid " , " musicid " , " timestamp " , name = " userid_musicid_timestamp " ) ,
mysql_charset = " utf8mb4 " ,
2019-12-08 22:43:49 +01:00
)
"""
Table for storing the mapping between game songid / chart and musicid for the score
and score_history table . To find scores , you will want to join this table with
the score table where id = score . musicid and game / version / songid / chart matches .
NOTE that it is expected to see the same songid / chart present multiple times as long
as the game version changes . In this way , a song which is in multiple versions of
the game can be found when playing each version .
"""
2021-05-31 20:07:03 +02:00
music = Table (
2022-10-15 20:56:30 +02:00
" music " ,
2019-12-08 22:43:49 +01:00
metadata ,
2022-10-15 20:56:30 +02:00
Column ( " id " , Integer , nullable = False , index = True ) ,
Column ( " songid " , Integer , nullable = False ) ,
Column ( " chart " , Integer , nullable = False ) ,
Column ( " game " , String ( 32 ) , nullable = False , index = True ) ,
Column ( " version " , Integer , nullable = False , index = True ) ,
Column ( " name " , String ( 255 ) ) ,
Column ( " artist " , String ( 255 ) ) ,
Column ( " genre " , String ( 255 ) ) ,
Column ( " data " , JSON ) ,
2024-01-02 03:46:24 +01:00
UniqueConstraint ( " songid " , " chart " , " game " , " version " , name = " songid_chart_game_version " ) ,
2022-10-15 20:56:30 +02:00
mysql_charset = " utf8mb4 " ,
2019-12-08 22:43:49 +01:00
)
class MusicData ( BaseData ) :
2024-01-02 03:46:24 +01:00
def __get_musicid ( self , game : GameConstants , version : int , songid : int , songchart : int ) - > int :
2019-12-08 22:43:49 +01:00
"""
Given a game / version / songid / chart , look up the unique music ID for this song .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
songid - ID of the song according to the game .
songchart - Chart number according to the game .
Returns :
Integer representing music ID if found or raises an exception otherwise .
"""
2022-10-15 20:56:30 +02:00
sql = " SELECT id FROM music WHERE songid = :songid AND chart = :chart AND game = :game AND version = :version "
cursor = self . execute (
sql ,
{
" songid " : songid ,
" chart " : songchart ,
" game " : game . value ,
" version " : version ,
} ,
2019-12-08 22:43:49 +01:00
)
if cursor . rowcount != 1 :
# music doesn't exist
2024-01-02 03:46:24 +01:00
raise Exception ( f " Song { songid } chart { songchart } doesn ' t exist for game { game } version { version } " )
2024-09-26 02:04:23 +02:00
result = cursor . mappings ( ) . fetchone ( ) # type: ignore
2022-10-15 20:56:30 +02:00
return result [ " id " ]
2019-12-08 22:43:49 +01:00
def put_score (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2019-12-08 22:43:49 +01:00
version : int ,
userid : UserID ,
songid : int ,
songchart : int ,
location : int ,
points : int ,
data : Dict [ str , Any ] ,
new_record : bool ,
2022-10-15 20:56:30 +02:00
timestamp : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > None :
"""
Given a game / version / song / chart and user ID , save a new / updated high score .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
songid - ID of the song according to the game .
songchart - Chart number according to the game .
location - Machine ID where this score was earned .
points - Points obtained on this song .
data - Data that the game wishes to record along with the score .
new_record - Whether this score was a new record or not .
timestamp - Optional integer specifying when the high score happened .
"""
# First look up the song/chart from the music DB
musicid = self . __get_musicid ( game , version , songid , songchart )
ts = timestamp if timestamp is not None else Time . now ( )
# Add to user score
if new_record :
# We want to update the timestamp/location to now if its a new record.
2023-03-19 19:09:06 +01:00
sql = """
INSERT INTO ` score ` ( ` userid ` , ` musicid ` , ` points ` , ` data ` , ` timestamp ` , ` update ` , ` lid ` )
VALUES ( : userid , : musicid , : points , : data , : timestamp , : update , : location )
ON DUPLICATE KEY UPDATE
data = VALUES ( data ) ,
points = VALUES ( points ) ,
` update ` = VALUES ( ` update ` ) ,
timestamp = VALUES ( timestamp ) ,
lid = VALUES ( lid )
"""
2019-12-08 22:43:49 +01:00
else :
2023-03-19 19:09:06 +01:00
# We don't want to add the timestamp of the record since it wasn't a new high score.
# We also don't want to update thet location since this wasn't a new record.
sql = """
INSERT INTO ` score ` ( ` userid ` , ` musicid ` , ` points ` , ` data ` , ` timestamp ` , ` update ` , ` lid ` )
VALUES ( : userid , : musicid , : points , : data , : timestamp , : update , : location )
ON DUPLICATE KEY UPDATE
data = VALUES ( data ) ,
points = VALUES ( points ) ,
` update ` = VALUES ( ` update ` )
"""
2019-12-08 22:43:49 +01:00
self . execute (
sql ,
{
2022-10-15 20:56:30 +02:00
" userid " : userid ,
" musicid " : musicid ,
" points " : points ,
" data " : self . serialize ( data ) ,
" timestamp " : ts ,
" update " : ts ,
" location " : location ,
} ,
2019-12-08 22:43:49 +01:00
)
def put_attempt (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2019-12-08 22:43:49 +01:00
version : int ,
userid : Optional [ UserID ] ,
songid : int ,
songchart : int ,
location : int ,
points : int ,
data : Dict [ str , Any ] ,
new_record : bool ,
2022-10-15 20:56:30 +02:00
timestamp : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > None :
"""
Given a game / version / song / chart and user ID , save a single score attempt .
Note that this is different than put_score above , because a user may have only one score
per song / chart in a given game , but they can have as many history entries as times played .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
songid - ID of the song according to the game .
songchart - Chart number according to the game .
location - Machine ID where this score was earned .
points - Points obtained on this song .
data - Optional data that the game wishes to record along with the score .
new_record - Whether this score was a new record or not .
timestamp - Optional integer specifying when the attempt happened .
"""
# First look up the song/chart from the music DB
musicid = self . __get_musicid ( game , version , songid , songchart )
ts = timestamp if timestamp is not None else Time . now ( )
# Add to score history
2023-03-19 19:09:06 +01:00
sql = """
INSERT INTO ` score_history ` ( userid , musicid , timestamp , lid , new_record , points , data )
VALUES ( : userid , : musicid , : timestamp , : location , : new_record , : points , : data )
"""
2019-12-08 22:43:49 +01:00
try :
self . execute (
sql ,
{
2022-10-15 20:56:30 +02:00
" userid " : userid if userid is not None else 0 ,
" musicid " : musicid ,
" timestamp " : ts ,
" location " : location ,
" new_record " : 1 if new_record else 0 ,
" points " : points ,
" data " : self . serialize ( data ) ,
2019-12-08 22:43:49 +01:00
} ,
)
except IntegrityError :
raise ScoreSaveException (
2022-10-15 20:56:30 +02:00
f " There is already an attempt by { userid if userid is not None else 0 } for music id { musicid } at { ts } "
2019-12-08 22:43:49 +01:00
)
2022-10-15 20:56:30 +02:00
def get_score (
self ,
game : GameConstants ,
version : int ,
userid : UserID ,
songid : int ,
songchart : int ,
) - > Optional [ Score ] :
2019-12-08 22:43:49 +01:00
"""
Look up a user ' s previous high score.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
songid - ID of the song according to the game .
songchart - Chart number according to the game .
Returns :
The optional data stored by the game previously , or None if no score exists .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
music . chart AS chart ,
score . id AS scorekey ,
score . timestamp AS timestamp ,
score . update AS ` update ` ,
score . lid AS lid ,
(
SELECT COUNT ( score_history . timestamp )
FROM score_history
WHERE score_history . musicid = music . id AND score_history . userid = : userid
) AS plays ,
score . points AS points ,
score . data AS data
FROM score , music
WHERE
score . userid = : userid AND
score . musicid = music . id AND
music . game = : game AND
music . version = : version AND
music . songid = : songid AND
music . chart = : songchart
"""
2019-12-08 22:43:49 +01:00
cursor = self . execute (
sql ,
{
2022-10-15 20:56:30 +02:00
" userid " : userid ,
" game " : game . value ,
" version " : version ,
" songid " : songid ,
" songchart " : songchart ,
2019-12-08 22:43:49 +01:00
} ,
)
if cursor . rowcount != 1 :
# score doesn't exist
return None
2024-09-26 02:04:23 +02:00
result = cursor . mappings ( ) . fetchone ( ) # type: ignore
2019-12-08 22:43:49 +01:00
return Score (
2022-10-15 20:56:30 +02:00
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " update " ] ,
result [ " lid " ] ,
result [ " plays " ] ,
self . deserialize ( result [ " data " ] ) ,
2019-12-08 22:43:49 +01:00
)
2024-01-02 03:46:24 +01:00
def get_score_by_key ( self , game : GameConstants , version : int , key : int ) - > Optional [ Tuple [ UserID , Score ] ] :
2019-12-08 22:43:49 +01:00
"""
Look up previous high score by key .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
key - Integer representing a unique key fetched in a previous Score lookup .
Returns :
The optional data stored by the game previously , or None if no score exists .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
music . chart AS chart ,
score . id AS scorekey ,
score . timestamp AS timestamp ,
score . update AS ` update ` ,
score . userid AS userid ,
score . lid AS lid ,
(
SELECT COUNT ( score_history . timestamp )
FROM score_history
WHERE score_history . musicid = music . id AND score_history . userid = score . userid
) AS plays ,
score . points AS points ,
score . data AS data
FROM score , music
WHERE
score . id = : scorekey AND
score . musicid = music . id AND
music . game = : game AND
music . version = : version
"""
2019-12-08 22:43:49 +01:00
cursor = self . execute (
sql ,
{
2022-10-15 20:56:30 +02:00
" game " : game . value ,
" version " : version ,
" scorekey " : key ,
2019-12-08 22:43:49 +01:00
} ,
)
if cursor . rowcount != 1 :
# score doesn't exist
return None
2024-09-26 02:04:23 +02:00
result = cursor . mappings ( ) . fetchone ( ) # type: ignore
2019-12-08 22:43:49 +01:00
return (
2022-10-15 20:56:30 +02:00
UserID ( result [ " userid " ] ) ,
2019-12-08 22:43:49 +01:00
Score (
2022-10-15 20:56:30 +02:00
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " update " ] ,
result [ " lid " ] ,
result [ " plays " ] ,
self . deserialize ( result [ " data " ] ) ,
) ,
2019-12-08 22:43:49 +01:00
)
def get_scores (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2019-12-08 22:43:49 +01:00
version : int ,
userid : UserID ,
2022-10-15 20:56:30 +02:00
since : Optional [ int ] = None ,
until : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Score ] :
"""
Look up all of a user ' s previous high scores.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
Returns :
A list of Score objects representing all high scores for a game .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
music . chart AS chart ,
score . id AS scorekey ,
score . timestamp AS timestamp ,
score . update AS ` update ` ,
score . lid AS lid ,
(
select COUNT ( score_history . timestamp ) FROM score_history
WHERE score_history . musicid = music . id AND score_history . userid = : userid
) AS plays ,
score . points AS points ,
score . data AS data
FROM score , music
WHERE
score . userid = : userid AND
score . musicid = music . id AND
music . game = : game AND
music . version = : version
"""
2019-12-08 22:43:49 +01:00
if since is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND score.update >= :since "
2019-12-08 22:43:49 +01:00
if until is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND score.update < :until "
cursor = self . execute (
sql ,
{
" userid " : userid ,
" game " : game . value ,
" version " : version ,
" since " : since ,
" until " : until ,
} ,
)
2019-12-08 22:43:49 +01:00
2023-03-19 06:23:35 +01:00
return [
Score (
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " update " ] ,
result [ " lid " ] ,
result [ " plays " ] ,
self . deserialize ( result [ " data " ] ) ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
for result in cursor . mappings ( )
2023-03-19 06:23:35 +01:00
]
2019-12-08 22:43:49 +01:00
2024-01-02 03:46:24 +01:00
def get_most_played ( self , game : GameConstants , version : int , userid : UserID , count : int ) - > List [ Tuple [ int , int ] ] :
2019-12-08 22:43:49 +01:00
"""
Look up a user ' s most played songs.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
count - Number of scores to look up .
Returns :
A list of tuples , containing the songid and the number of plays across all charts for that song .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
COUNT ( score_history . timestamp ) AS plays
FROM score_history , music
WHERE
score_history . userid = : userid AND
score_history . musicid = music . id AND
music . game = : game AND
music . version = : version
GROUP BY songid ORDER BY plays DESC LIMIT : count
"""
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{ " userid " : userid , " game " : game . value , " version " : version , " count " : count } ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
return [ ( result [ " songid " ] , result [ " plays " ] ) for result in cursor . mappings ( ) ]
2019-12-08 22:43:49 +01:00
2024-01-02 03:46:24 +01:00
def get_last_played ( self , game : GameConstants , version : int , userid : UserID , count : int ) - > List [ Tuple [ int , int ] ] :
2019-12-08 22:43:49 +01:00
"""
Look up a user ' s last played songs.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userid - Integer representing a user . Usually looked up with UserData .
count - Number of scores to look up .
Returns :
A list of tuples , containing the songid and the last played time for this song .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
DISTINCT ( music . songid ) AS songid ,
score_history . timestamp AS timestamp
FROM score_history , music
WHERE
score_history . userid = : userid AND
score_history . musicid = music . id AND
music . game = : game AND
music . version = : version
ORDER BY timestamp DESC LIMIT : count
"""
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{ " userid " : userid , " game " : game . value , " version " : version , " count " : count } ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
return [ ( result [ " songid " ] , result [ " timestamp " ] ) for result in cursor . mappings ( ) ]
2019-12-08 22:43:49 +01:00
def get_hit_chart (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2019-12-08 22:43:49 +01:00
version : int ,
count : int ,
2022-10-15 20:56:30 +02:00
days : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Tuple [ int , int ] ] :
"""
Look up a game ' s most played songs.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
count - Number of scores to look up .
Returns :
A list of tuples , containing the songid and the number of plays across all charts for that song .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
COUNT ( score_history . timestamp ) AS plays
FROM score_history , music
WHERE
score_history . musicid = music . id AND
music . game = : game AND
music . version = : version
"""
2021-05-31 20:14:51 +02:00
timestamp : Optional [ int ] = None
2019-12-08 22:43:49 +01:00
if days is not None :
# Only select the last X days of hit chart
sql = sql + " AND score_history.timestamp > :timestamp "
timestamp = Time . now ( ) - ( Time . SECONDS_IN_DAY * days )
sql = sql + " GROUP BY songid ORDER BY plays DESC LIMIT :count "
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{
" game " : game . value ,
" version " : version ,
" count " : count ,
" timestamp " : timestamp ,
} ,
)
2019-12-08 22:43:49 +01:00
2024-09-26 02:04:23 +02:00
return [ ( result [ " songid " ] , result [ " plays " ] ) for result in cursor . mappings ( ) ]
2019-12-08 22:43:49 +01:00
def get_song (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2019-12-08 22:43:49 +01:00
version : int ,
songid : int ,
songchart : int ,
) - > Optional [ Song ] :
"""
Given a game / version / songid / chart , look up the name , artist and genre of that song .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
songid - Integer representing the ID ( from the game ) for this song .
songchart - Integer representing the chart for this song .
Returns :
A Song object representing the song details
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . name AS name ,
music . artist AS artist ,
music . genre AS genre ,
music . data AS data
FROM music
WHERE
music . game = : game AND
music . version = : version AND
music . songid = : songid AND
music . chart = : songchart
"""
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{
" game " : game . value ,
" version " : version ,
" songid " : songid ,
" songchart " : songchart ,
} ,
2019-12-08 22:43:49 +01:00
)
if cursor . rowcount != 1 :
# music doesn't exist
return None
2024-09-26 02:04:23 +02:00
result = cursor . mappings ( ) . fetchone ( ) # type: ignore
2019-12-08 22:43:49 +01:00
return Song (
game ,
version ,
songid ,
songchart ,
2022-10-15 20:56:30 +02:00
result [ " name " ] ,
result [ " artist " ] ,
result [ " genre " ] ,
self . deserialize ( result [ " data " ] ) ,
2019-12-08 22:43:49 +01:00
)
def get_all_songs (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2022-10-15 20:56:30 +02:00
version : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Song ] :
"""
Given a game and a version , look up all song / chart combos associated with that game .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
Returns :
A list of Song objects detailing the song information for each song .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT version , songid , chart , name , artist , genre , data
FROM music WHERE music . game = : game
"""
2022-10-15 20:56:30 +02:00
params : Dict [ str , Any ] = { " game " : game . value }
2019-12-08 22:43:49 +01:00
if version is not None :
sql + = " AND music.version = :version "
2022-10-15 20:56:30 +02:00
params [ " version " ] = version
2019-12-08 22:43:49 +01:00
else :
sql + = " ORDER BY music.version DESC "
cursor = self . execute ( sql , params )
2023-03-19 06:23:35 +01:00
return [
Song (
game ,
result [ " version " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " name " ] ,
result [ " artist " ] ,
result [ " genre " ] ,
self . deserialize ( result [ " data " ] ) ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
for result in cursor . mappings ( )
2023-03-19 06:23:35 +01:00
]
2019-12-08 22:43:49 +01:00
def get_all_scores (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2022-10-15 20:56:30 +02:00
version : Optional [ int ] = None ,
userid : Optional [ UserID ] = None ,
songid : Optional [ int ] = None ,
songchart : Optional [ int ] = None ,
since : Optional [ int ] = None ,
until : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Tuple [ UserID , Score ] ] :
"""
Look up all of a game ' s high scores for all users.
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
Returns :
A list of UserID , Score objects representing all high scores for a game .
"""
# First, construct the queries for grabbing the songid/chart
if version is not None :
2024-01-02 03:46:24 +01:00
songidquery = (
" SELECT songid FROM music WHERE music.id = score.musicid AND game = :game AND version = :version "
)
chartquery = (
" SELECT chart FROM music WHERE music.id = score.musicid AND game = :game AND version = :version "
)
2019-12-08 22:43:49 +01:00
else :
2024-01-02 03:46:24 +01:00
songidquery = (
" SELECT songid FROM music WHERE music.id = score.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
)
chartquery = (
" SELECT chart FROM music WHERE music.id = score.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
)
2019-12-08 22:43:49 +01:00
# Select statement for getting play count
2022-10-15 20:56:30 +02:00
playselect = " SELECT COUNT(timestamp) FROM score_history WHERE score_history.musicid = score.musicid AND score_history.userid = score.userid "
2019-12-08 22:43:49 +01:00
# Now, construct the inner select statement so we can choose which scores we care about
2022-10-15 20:56:30 +02:00
innerselect = " SELECT DISTINCT(id) FROM music WHERE game = :game "
2019-12-08 22:43:49 +01:00
if version is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND version = :version "
2019-12-08 22:43:49 +01:00
if songid is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND songid = :songid "
2019-12-08 22:43:49 +01:00
if songchart is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND chart = :songchart "
2019-12-08 22:43:49 +01:00
# Finally, construct the full query
2023-03-19 19:09:06 +01:00
sql = f """
SELECT
( { songidquery } ) AS songid ,
( { chartquery } ) AS chart ,
id AS scorekey ,
points ,
timestamp ,
` update ` ,
lid ,
data ,
userid ,
( { playselect } ) AS plays
FROM score WHERE musicid IN ( { innerselect } )
"""
2019-12-08 22:43:49 +01:00
# Now, limit the query
if userid is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND userid = :userid "
2019-12-08 22:43:49 +01:00
if since is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND score.update >= :since "
2019-12-08 22:43:49 +01:00
if until is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND score.update < :until "
2019-12-08 22:43:49 +01:00
# Now, query itself
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{
" game " : game . value ,
" version " : version ,
" userid " : userid ,
" songid " : songid ,
" songchart " : songchart ,
" since " : since ,
" until " : until ,
} ,
)
2019-12-08 22:43:49 +01:00
# Objectify result
2023-03-19 06:23:35 +01:00
return [
(
UserID ( result [ " userid " ] ) ,
Score (
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " update " ] ,
result [ " lid " ] ,
result [ " plays " ] ,
self . deserialize ( result [ " data " ] ) ,
) ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
for result in cursor . mappings ( )
2023-03-19 06:23:35 +01:00
]
2019-12-08 22:43:49 +01:00
def get_all_records (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2022-10-15 20:56:30 +02:00
version : Optional [ int ] = None ,
userlist : Optional [ List [ UserID ] ] = None ,
locationlist : Optional [ List [ int ] ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Tuple [ UserID , Score ] ] :
"""
Look up all of a game ' s records, only returning the top score for each song. For score ties,
king - of - the - hill rules are in effect , so for two players with an identical top score , the player
that got the score last wins . If a list of user IDs is given , we will only look up records pertaining
to those users . So if another user has a higher record , we will ignore this . This can be used to
display area - local high scores , etc .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
userlist - List of UserIDs to limit the search to .
locationlist - A list of location IDs to limit searches to .
Returns :
A list of UserID , Score objects representing all high scores for a game .
"""
# First, construct the queries for grabbing the songid/chart
if version is not None :
2024-01-02 03:46:24 +01:00
songidquery = (
" SELECT songid FROM music WHERE music.id = score.musicid AND game = :game AND version = :version "
)
chartquery = (
" SELECT chart FROM music WHERE music.id = score.musicid AND game = :game AND version = :version "
)
2019-12-08 22:43:49 +01:00
else :
2024-01-02 03:46:24 +01:00
songidquery = (
" SELECT songid FROM music WHERE music.id = score.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
)
chartquery = (
" SELECT chart FROM music WHERE music.id = score.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
)
2019-12-08 22:43:49 +01:00
# Next, get a list of all songs that were played given the input criteria
2024-01-02 03:46:24 +01:00
musicid_sql = (
" SELECT DISTINCT(score.musicid) FROM score, music WHERE score.musicid = music.id AND music.game = :game "
)
2022-10-15 20:56:30 +02:00
params : Dict [ str , Any ] = { " game " : game . value }
2019-12-08 22:43:49 +01:00
if version is not None :
2022-10-15 20:56:30 +02:00
musicid_sql = musicid_sql + " AND music.version = :version "
params [ " version " ] = version
2019-12-08 22:43:49 +01:00
# Figure out where the record was earned
if locationlist is not None :
if len ( locationlist ) == 0 :
# We don't have any locations, but SQL will shit the bed, so lets add a default one.
locationlist . append ( - 1 )
location_sql = " AND score.lid IN :locationlist "
2022-10-15 20:56:30 +02:00
params [ " locationlist " ] = tuple ( locationlist )
2019-12-08 22:43:49 +01:00
else :
location_sql = " "
# Figure out who got the record
if userlist is not None :
if len ( userlist ) == 0 :
# We don't have any users, but SQL will shit the bed, so lets add a fake one.
userlist . append ( UserID ( - 1 ) )
2020-01-07 22:29:07 +01:00
user_sql = f " SELECT userid FROM score WHERE score.musicid = played.musicid AND score.userid IN :userlist { location_sql } ORDER BY points DESC, timestamp DESC LIMIT 1 "
2022-10-15 20:56:30 +02:00
params [ " userlist " ] = tuple ( userlist )
2019-12-08 22:43:49 +01:00
else :
2020-01-07 22:29:07 +01:00
user_sql = f " SELECT userid FROM score WHERE score.musicid = played.musicid { location_sql } ORDER BY points DESC, timestamp DESC LIMIT 1 "
2023-03-19 19:09:06 +01:00
records_sql = f """
SELECT ( { user_sql } ) AS userid , musicid
FROM ( { musicid_sql } ) played
"""
2019-12-08 22:43:49 +01:00
# Now, join it up against the score and music table to grab the info we need
2023-03-19 19:09:06 +01:00
sql = f """
SELECT
( { songidquery } ) AS songid ,
( { chartquery } ) AS chart ,
score . points AS points ,
score . userid AS userid ,
score . id AS scorekey ,
score . data AS data ,
score . timestamp AS timestamp ,
score . update AS ` update ` ,
score . lid AS lid ,
(
SELECT COUNT ( score_history . timestamp ) FROM score_history
WHERE score_history . musicid = score . musicid
) AS plays
FROM score , ( { records_sql } ) records
WHERE records . userid = score . userid AND records . musicid = score . musicid
"""
2019-12-08 22:43:49 +01:00
cursor = self . execute ( sql , params )
2023-03-19 06:23:35 +01:00
return [
(
UserID ( result [ " userid " ] ) ,
Score (
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " update " ] ,
result [ " lid " ] ,
result [ " plays " ] ,
self . deserialize ( result [ " data " ] ) ,
) ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
for result in cursor . mappings ( )
2023-03-19 06:23:35 +01:00
]
2019-12-08 22:43:49 +01:00
2024-01-02 03:46:24 +01:00
def get_attempt_by_key ( self , game : GameConstants , version : int , key : int ) - > Optional [ Tuple [ UserID , Attempt ] ] :
2019-12-08 22:43:49 +01:00
"""
Look up a previous attempt by key .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
key - Integer representing a unique key fetched in a previous Attempt lookup .
Returns :
The optional data stored by the game previously , or None if no score exists .
"""
2023-03-19 19:09:06 +01:00
sql = """
SELECT
music . songid AS songid ,
music . chart AS chart ,
score_history . id AS scorekey ,
score_history . timestamp AS timestamp ,
score_history . userid AS userid ,
score_history . lid AS lid ,
score_history . new_record AS new_record ,
score_history . points AS points ,
score_history . data AS data
FROM score_history , music
WHERE
score_history . id = : scorekey AND
score_history . musicid = music . id AND
music . game = : game AND
music . version = : version
"""
2019-12-08 22:43:49 +01:00
cursor = self . execute (
sql ,
{
2022-10-15 20:56:30 +02:00
" game " : game . value ,
" version " : version ,
" scorekey " : key ,
2019-12-08 22:43:49 +01:00
} ,
)
if cursor . rowcount != 1 :
# score doesn't exist
return None
2024-09-26 02:04:23 +02:00
result = cursor . mappings ( ) . fetchone ( ) # type: ignore
2019-12-08 22:43:49 +01:00
return (
2022-10-15 20:56:30 +02:00
UserID ( result [ " userid " ] ) ,
2019-12-08 22:43:49 +01:00
Attempt (
2022-10-15 20:56:30 +02:00
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " lid " ] ,
2023-03-19 06:40:52 +01:00
result [ " new_record " ] == 1 ,
2022-10-15 20:56:30 +02:00
self . deserialize ( result [ " data " ] ) ,
) ,
2019-12-08 22:43:49 +01:00
)
def get_all_attempts (
self ,
2021-08-19 21:21:22 +02:00
game : GameConstants ,
2022-10-15 20:56:30 +02:00
version : Optional [ int ] = None ,
userid : Optional [ UserID ] = None ,
songid : Optional [ int ] = None ,
songchart : Optional [ int ] = None ,
timelimit : Optional [ int ] = None ,
limit : Optional [ int ] = None ,
offset : Optional [ int ] = None ,
2019-12-08 22:43:49 +01:00
) - > List [ Tuple [ Optional [ UserID ] , Attempt ] ] :
"""
Look up all of the attempts to score for a particular game .
Parameters :
2021-08-19 21:21:22 +02:00
game - Enum value representing a game series .
2019-12-08 22:43:49 +01:00
version - Integer representing which version of the game .
Returns :
A list of UserID , Attempt objects representing all score attempts for a game , sorted newest to oldest attempts .
"""
# First, construct the queries for grabbing the songid/chart
if version is not None :
2022-10-15 20:56:30 +02:00
songidquery = " SELECT songid FROM music WHERE music.id = score_history.musicid AND game = :game AND version = :version "
2024-01-02 03:46:24 +01:00
chartquery = (
" SELECT chart FROM music WHERE music.id = score_history.musicid AND game = :game AND version = :version "
)
2019-12-08 22:43:49 +01:00
else :
2022-10-15 20:56:30 +02:00
songidquery = " SELECT songid FROM music WHERE music.id = score_history.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
chartquery = " SELECT chart FROM music WHERE music.id = score_history.musicid AND game = :game ORDER BY version DESC LIMIT 1 "
2019-12-08 22:43:49 +01:00
# Now, construct the inner select statement so we can choose which scores we care about
2022-10-15 20:56:30 +02:00
innerselect = " SELECT DISTINCT(id) FROM music WHERE game = :game "
2019-12-08 22:43:49 +01:00
if version is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND version = :version "
2019-12-08 22:43:49 +01:00
if songid is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND songid = :songid "
2019-12-08 22:43:49 +01:00
if songchart is not None :
2022-10-15 20:56:30 +02:00
innerselect = innerselect + " AND chart = :songchart "
2019-12-08 22:43:49 +01:00
# Finally, construct the full query
2023-03-19 19:09:06 +01:00
sql = f """
SELECT
( { songidquery } ) AS songid ,
( { chartquery } ) AS chart ,
id AS scorekey ,
timestamp ,
points ,
new_record ,
lid ,
data ,
userid
FROM score_history WHERE musicid IN ( { innerselect } )
"""
2019-12-08 22:43:49 +01:00
# Now, limit the query
if userid is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND userid = :userid "
2019-12-08 22:43:49 +01:00
if timelimit is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " AND timestamp >= :timestamp "
sql = sql + " ORDER BY timestamp DESC "
2019-12-08 22:43:49 +01:00
if limit is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " LIMIT :limit "
2019-12-08 22:43:49 +01:00
if offset is not None :
2022-10-15 20:56:30 +02:00
sql = sql + " OFFSET :offset "
2019-12-08 22:43:49 +01:00
# Now, query itself
2022-10-15 20:56:30 +02:00
cursor = self . execute (
sql ,
{
" game " : game . value ,
" version " : version ,
" userid " : userid ,
" songid " : songid ,
" songchart " : songchart ,
" timestamp " : timelimit ,
" limit " : limit ,
" offset " : offset ,
} ,
)
2019-12-08 22:43:49 +01:00
# Now objectify the attempts
2023-03-19 06:23:35 +01:00
return [
(
UserID ( result [ " userid " ] ) if result [ " userid " ] > 0 else None ,
Attempt (
result [ " scorekey " ] ,
result [ " songid " ] ,
result [ " chart " ] ,
result [ " points " ] ,
result [ " timestamp " ] ,
result [ " lid " ] ,
2023-03-19 06:40:52 +01:00
result [ " new_record " ] == 1 ,
2023-03-19 06:23:35 +01:00
self . deserialize ( result [ " data " ] ) ,
) ,
2019-12-08 22:43:49 +01:00
)
2024-09-26 02:04:23 +02:00
for result in cursor . mappings ( )
2023-03-19 06:23:35 +01:00
]