- 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>
392 lines
12 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|