opsu-dance/src/itdelatrisu/opsu/db/ScoreDB.java
Jeffrey Han d860a30aed Follow-up to .
- Many code style changes.
- Don't increment combo if missing the last slider circle.
- Added player name in ranking screen.
- Don't show null/default player names.
- Only import replays with .osr extension.
- Display loading status for importing replays.
- Moved MD5InputStreamWrapper to package "opsu.io".

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2015-06-29 19:22:38 -05:00

392 lines
12 KiB
Java

/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.db;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.beatmap.Beatmap;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Handles connections and queries with the scores database.
*/
public class ScoreDB {
/**
* Current database version.
* This value should be changed whenever the database format changes.
* Add any update queries to the {@link #getUpdateQueries(int)} method.
*/
private static final int DATABASE_VERSION = 20150401;
/**
* Returns a list of SQL queries to apply, in order, to update from
* the given database version to the latest version.
* @param version the current version
* @return a list of SQL queries
*/
private static List<String> getUpdateQueries(int version) {
List<String> list = new LinkedList<String>();
if (version < 20140311)
list.add("ALTER TABLE scores ADD COLUMN replay TEXT");
if (version < 20150401)
list.add("ALTER TABLE scores ADD COLUMN playerName TEXT");
/* add future updates here */
return list;
}
/** Database connection. */
private static Connection connection;
/** Score insertion statement. */
private static PreparedStatement insertStmt;
/** Score select statement. */
private static PreparedStatement selectMapStmt, selectMapSetStmt;
/** Score deletion statement. */
private static PreparedStatement deleteSongStmt, deleteScoreStmt;
// This class should not be instantiated.
private ScoreDB() {}
/**
* Initializes the database connection.
*/
public static void init() {
// create a database connection
connection = DBController.createConnection(Options.SCORE_DB.getPath());
if (connection == null)
return;
// run any database updates
updateDatabase();
// create the database
createDatabase();
// prepare sql statements
try {
insertStmt = connection.prepareStatement(
// TODO: There will be problems if multiple replays have the same
// timestamp (e.g. when imported) due to timestamp being the primary key.
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
selectMapSetStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MSID = ? AND title = ? AND artist = ? AND creator = ? ORDER BY version DESC"
);
deleteSongStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
deleteScoreStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"replay = ? AND playerName = ?"
);
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare score statements.", e, true);
}
}
/**
* Creates the database, if it does not exist.
*/
private static void createDatabase() {
try (Statement stmt = connection.createStatement()) {
String sql =
"CREATE TABLE IF NOT EXISTS scores (" +
"timestamp INTEGER PRIMARY KEY, " +
"MID INTEGER, MSID INTEGER, " +
"title TEXT, artist TEXT, creator TEXT, version TEXT, " +
"hit300 INTEGER, hit100 INTEGER, hit50 INTEGER, " +
"geki INTEGER, katu INTEGER, miss INTEGER, " +
"score INTEGER, " +
"combo INTEGER, " +
"perfect BOOLEAN, " +
"mods INTEGER, " +
"replay TEXT, " +
"playerName TEXT"+
");" +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
"); " +
"CREATE INDEX IF NOT EXISTS idx ON scores (MID, MSID, title, artist, creator, version);";
stmt.executeUpdate(sql);
// set the version key, if empty
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', %d)", DATABASE_VERSION);
stmt.executeUpdate(sql);
} catch (SQLException e) {
ErrorHandler.error("Could not create score database.", e, true);
}
}
/**
* Applies any database updates by comparing the current version to the
* stored version. Does nothing if tables have not been created.
*/
private static void updateDatabase() {
try (Statement stmt = connection.createStatement()) {
int version = 0;
// if 'info' table does not exist, assume version 0 and apply all updates
String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
ResultSet rs = stmt.executeQuery(sql);
boolean infoExists = rs.isBeforeFirst();
rs.close();
if (!infoExists) {
// if 'scores' table also does not exist, databases not yet created
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'scores'";
ResultSet scoresRS = stmt.executeQuery(sql);
boolean scoresExists = scoresRS.isBeforeFirst();
scoresRS.close();
if (!scoresExists)
return;
} else {
// try to retrieve stored version
sql = "SELECT value FROM info WHERE key = 'version'";
ResultSet versionRS = stmt.executeQuery(sql);
String versionString = (versionRS.next()) ? versionRS.getString(1) : "0";
versionRS.close();
try {
version = Integer.parseInt(versionString);
} catch (NumberFormatException e) {}
}
// database versions match
if (version >= DATABASE_VERSION)
return;
// apply updates
for (String query : getUpdateQueries(version))
stmt.executeUpdate(query);
// update version
if (infoExists) {
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
ps.setString(1, Integer.toString(DATABASE_VERSION));
ps.executeUpdate();
ps.close();
}
} catch (SQLException e) {
ErrorHandler.error("Failed to update score database.", e, true);
}
}
/**
* Adds the game score to the database.
* @param data the GameData object
*/
public static void addScore(ScoreData data) {
if (connection == null)
return;
try {
setStatementFields(insertStmt, data);
insertStmt.setString(18, data.replayString);
insertStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to save score to database.", e, true);
}
}
/**
* Deletes the given score from the database.
* @param data the score to delete
*/
public static void deleteScore(ScoreData data) {
if (connection == null)
return;
try {
setStatementFields(deleteScoreStmt, data);
deleteScoreStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to delete score from database.", e, true);
}
}
/**
* Deletes all the scores for the given beatmap from the database.
* @param beatmap the beatmap
*/
public static void deleteScore(Beatmap beatmap) {
if (connection == null)
return;
try {
deleteSongStmt.setInt(1, beatmap.beatmapID);
deleteSongStmt.setString(2, beatmap.title);
deleteSongStmt.setString(3, beatmap.artist);
deleteSongStmt.setString(4, beatmap.creator);
deleteSongStmt.setString(5, beatmap.version);
deleteSongStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to delete scores from database.", e, true);
}
}
/**
* Sets all statement fields using a given ScoreData object.
* @param stmt the statement to set fields for
* @param data the score data
* @throws SQLException
*/
private static void setStatementFields(PreparedStatement stmt, ScoreData data)
throws SQLException {
stmt.setLong(1, data.timestamp);
stmt.setInt(2, data.MID);
stmt.setInt(3, data.MSID);
stmt.setString(4, data.title);
stmt.setString(5, data.artist);
stmt.setString(6, data.creator);
stmt.setString(7, data.version);
stmt.setInt(8, data.hit300);
stmt.setInt(9, data.hit100);
stmt.setInt(10, data.hit50);
stmt.setInt(11, data.geki);
stmt.setInt(12, data.katu);
stmt.setInt(13, data.miss);
stmt.setLong(14, data.score);
stmt.setInt(15, data.combo);
stmt.setBoolean(16, data.perfect);
stmt.setInt(17, data.mods);
stmt.setString(18, data.replayString);
stmt.setString(19, data.playerName);
}
/**
* Retrieves the game scores for a beatmap.
* @param beatmap the beatmap
* @return all scores for the beatmap, or null if any error occurred
*/
public static ScoreData[] getMapScores(Beatmap beatmap) {
if (connection == null)
return null;
List<ScoreData> list = new ArrayList<ScoreData>();
try {
selectMapStmt.setInt(1, beatmap.beatmapID);
selectMapStmt.setString(2, beatmap.title);
selectMapStmt.setString(3, beatmap.artist);
selectMapStmt.setString(4, beatmap.creator);
selectMapStmt.setString(5, beatmap.version);
ResultSet rs = selectMapStmt.executeQuery();
while (rs.next()) {
ScoreData s = new ScoreData(rs);
list.add(s);
}
rs.close();
} catch (SQLException e) {
ErrorHandler.error("Failed to read scores from database.", e, true);
return null;
}
return getSortedArray(list);
}
/**
* Retrieves the game scores for a beatmap set.
* @param beatmap the beatmap
* @return all scores for the beatmap set (Version, ScoreData[]),
* or null if any error occurred
*/
public static Map<String, ScoreData[]> getMapSetScores(Beatmap beatmap) {
if (connection == null)
return null;
Map<String, ScoreData[]> map = new HashMap<String, ScoreData[]>();
try {
selectMapSetStmt.setInt(1, beatmap.beatmapSetID);
selectMapSetStmt.setString(2, beatmap.title);
selectMapSetStmt.setString(3, beatmap.artist);
selectMapSetStmt.setString(4, beatmap.creator);
ResultSet rs = selectMapSetStmt.executeQuery();
List<ScoreData> list = null;
String version = ""; // sorted by version, so pass through and check for differences
while (rs.next()) {
ScoreData s = new ScoreData(rs);
if (!s.version.equals(version)) {
if (list != null)
map.put(version, getSortedArray(list));
version = s.version;
list = new ArrayList<ScoreData>();
}
list.add(s);
}
if (list != null)
map.put(version, getSortedArray(list));
rs.close();
} catch (SQLException e) {
ErrorHandler.error("Failed to read scores from database.", e, true);
return null;
}
return map;
}
/**
* Returns a sorted ScoreData array (in reverse order) from a List.
*/
private static ScoreData[] getSortedArray(List<ScoreData> list) {
ScoreData[] scores = list.toArray(new ScoreData[list.size()]);
Arrays.sort(scores, Collections.reverseOrder());
return scores;
}
/**
* Closes the connection to the database.
*/
public static void closeConnection() {
if (connection == null)
return;
try {
insertStmt.close();
selectMapStmt.close();
selectMapSetStmt.close();
connection.close();
connection = null;
} catch (SQLException e) {
ErrorHandler.error("Failed to close score database.", e, true);
}
}
}