From bf04083ebd92fe1651a15892e586a8ebb4d2159a Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Wed, 4 Mar 2015 21:03:06 -0500 Subject: [PATCH] Added a beatmap cache database. - New database ".opsu.db" stores a cached copy of all parsed beatmaps. All data will be read from this database unless the last modified time of a beatmap file does not match the one in the table. - OsuParser inserts all new entries to the database in batch after parsing. - Added *toString()/*fromString() methods for 'breaks', 'timingPoints', and 'combo' fields in OsuFile for use with the database. - For any database format changes, update the DATABASE_VERSION field in OsuDB. - Reloading beatmaps (F5) will now clear the beatmap cache. Related changes: - Added small DBController class for convenience. - Changed 'bg' field of OsuFile to only contain the image file name, instead of the full path. - Deleted printDatabase() method from ScoreDB. Signed-off-by: Jeffrey Han --- .gitignore | 1 + src/itdelatrisu/opsu/Opsu.java | 9 +- src/itdelatrisu/opsu/Options.java | 3 + src/itdelatrisu/opsu/OsuFile.java | 117 ++++++- src/itdelatrisu/opsu/OsuParser.java | 50 ++- src/itdelatrisu/opsu/OsuTimingPoint.java | 12 + src/itdelatrisu/opsu/Utils.java | 2 +- src/itdelatrisu/opsu/db/DBController.java | 53 ++++ src/itdelatrisu/opsu/db/OsuDB.java | 352 +++++++++++++++++++++ src/itdelatrisu/opsu/{ => db}/ScoreDB.java | 37 +-- src/itdelatrisu/opsu/states/Game.java | 2 +- src/itdelatrisu/opsu/states/SongMenu.java | 6 +- 12 files changed, 599 insertions(+), 45 deletions(-) create mode 100644 src/itdelatrisu/opsu/db/DBController.java create mode 100644 src/itdelatrisu/opsu/db/OsuDB.java rename src/itdelatrisu/opsu/{ => db}/ScoreDB.java (90%) diff --git a/.gitignore b/.gitignore index 5fcfbfab..ebdc524f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /Songs/ /.opsu.log /.opsu.cfg +/.opsu.db /.opsu_scores.db # Eclipse diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 1a81726c..56748665 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; +import itdelatrisu.opsu.db.DBController; import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.states.ButtonMenu; import itdelatrisu.opsu.states.DownloadsMenu; @@ -132,8 +133,8 @@ public class Opsu extends StateBasedGame { ResourceLoader.addResourceLocation(new FileSystemLocation(new File("."))); ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/"))); - // initialize score database - ScoreDB.init(); + // initialize databases + DBController.init(); // start the game try { @@ -200,8 +201,8 @@ public class Opsu extends StateBasedGame { * Closes all resources and exits the application. */ public static void exit() { - // close scores database - ScoreDB.closeConnection(); + // close databases + DBController.closeConnections(); // cancel all downloads DownloadList.get().cancelAllDownloads(); diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index ae73c61b..43e6bebf 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -61,6 +61,9 @@ public class Options { new File(DATA_DIR, "Songs/").getPath() }; + /** Cached beatmap database name. */ + public static final File OSU_DB = new File(DATA_DIR, ".opsu.db"); + /** Score database name. */ public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db"); diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index d61c5156..a8c09f57 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -21,6 +21,7 @@ package itdelatrisu.opsu; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import org.newdawn.slick.Color; import org.newdawn.slick.Image; @@ -164,10 +165,10 @@ public class OsuFile implements Comparable { */ /** All timing points. */ - public ArrayList timingPoints; + public ArrayList timingPoints = new ArrayList(); /** Song BPM range. */ - int bpmMin = 0, bpmMax = 0; + public int bpmMin = 0, bpmMax = 0; /** * [Colours] @@ -264,7 +265,7 @@ public class OsuFile implements Comparable { if (bgImage == null) { if (bgImageMap.size() > MAX_CACHE_SIZE) clearImageCache(); - bgImage = new Image(bg); + bgImage = new Image(new File(file.getParentFile(), bg).getAbsolutePath()); bgImageMap.put(this, bgImage); } @@ -317,4 +318,114 @@ public class OsuFile implements Comparable { public String toString() { return String.format("%s - %s [%s]", getArtist(), getTitle(), version); } + + /** + * Returns the {@link #breaks} field formatted as a string, + * or null if the field is null. + */ + public String breaksToString() { + if (breaks == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i : breaks) { + sb.append(i); + sb.append(','); + } + if (sb.length() > 0) + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + /** + * Sets the {@link #breaks} field from a string. + * @param s the string + */ + public void breaksFromString(String s) { + if (s == null) + return; + + this.breaks = new ArrayList(); + String[] tokens = s.split(","); + for (int i = 0; i < tokens.length; i++) + breaks.add(Integer.parseInt(tokens[i])); + } + + /** + * Returns the {@link #timingPoints} field formatted as a string, + * or null if the field is null. + */ + public String timingPointsToString() { + if (timingPoints == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (OsuTimingPoint p : timingPoints) { + sb.append(p.toString()); + sb.append('|'); + } + if (sb.length() > 0) + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + /** + * Sets the {@link #timingPoints} field from a string. + * @param s the string + */ + public void timingPointsFromString(String s) { + if (s == null) + return; + + String[] tokens = s.split("\\|"); + for (int i = 0; i < tokens.length; i++) { + try { + timingPoints.add(new OsuTimingPoint(tokens[i])); + } catch (Exception e) { + Log.warn(String.format("Failed to read timing point '%s'.", tokens[i]), e); + } + } + timingPoints.trimToSize(); + } + + /** + * Returns the {@link #combo} field formatted as a string, + * or null if the field is null. + */ + public String comboToString() { + if (combo == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < combo.length; i++) { + Color c = combo[i]; + sb.append(c.getRed()); + sb.append(','); + sb.append(c.getGreen()); + sb.append(','); + sb.append(c.getBlue()); + sb.append('|'); + } + if (sb.length() > 0) + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + /** + * Sets the {@link #combo} field from a string. + * @param s the string + */ + public void comboFromString(String s) { + if (s == null) + return; + + LinkedList colors = new LinkedList(); + String[] tokens = s.split("\\|"); + for (int i = 0; i < tokens.length; i++) { + String[] rgb = tokens[i].split(","); + colors.add(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]))); + } + if (!colors.isEmpty()) + this.combo = colors.toArray(new Color[colors.size()]); + } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/OsuParser.java b/src/itdelatrisu/opsu/OsuParser.java index e0a85f43..920a9bcb 100644 --- a/src/itdelatrisu/opsu/OsuParser.java +++ b/src/itdelatrisu/opsu/OsuParser.java @@ -18,6 +18,8 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.db.OsuDB; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -29,6 +31,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; +import java.util.Map; import org.newdawn.slick.Color; import org.newdawn.slick.util.Log; @@ -52,6 +55,9 @@ public class OsuParser { /** The total number of directories to parse. */ private static int totalDirectories = -1; + /** Whether or not the database is currently being updated. */ + private static boolean updatingDatabase = false; + // This class should not be instantiated. private OsuParser() {} @@ -82,7 +88,11 @@ public class OsuParser { currentDirectoryIndex = 0; totalDirectories = dirs.length; + // get last modified map from database + Map map = OsuDB.getLastModifiedMap(); + // parse directories + LinkedList parsedOsuFiles = new LinkedList(); OsuGroupNode lastNode = null; for (File dir : dirs) { currentDirectoryIndex++; @@ -104,9 +114,24 @@ public class OsuParser { for (File file : files) { currentFile = file; + // check if beatmap is cached + String path = String.format("%s/%s", dir.getName(), file.getName()); + if (map.containsKey(path)) { + // check last modified times + long lastModified = map.get(path); + if (lastModified == file.lastModified()) { + osuFiles.add(OsuDB.getOsuFile(dir, file)); + continue; + } else + OsuDB.delete(dir.getName(), file.getName()); + } + // Parse hit objects only when needed to save time/memory. // Change boolean to 'true' to parse them immediately. - parseFile(file, dir, osuFiles, false); + OsuFile osu = parseFile(file, dir, osuFiles, false); + + if (osu != null) + parsedOsuFiles.add(osu); } if (!osuFiles.isEmpty()) { // add entry if non-empty osuFiles.trimToSize(); @@ -122,6 +147,11 @@ public class OsuParser { // clear string DB stringdb = new HashMap(); + // add entries to database + updatingDatabase = true; + OsuDB.insert(parsedOsuFiles); + updatingDatabase = false; + currentFile = null; currentDirectoryIndex = -1; totalDirectories = -1; @@ -140,10 +170,6 @@ public class OsuParser { OsuFile osu = new OsuFile(file); try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) { - - // initialize timing point list - osu.timingPoints = new ArrayList(); - String line = in.readLine(); String tokens[] = null; while (line != null) { @@ -362,7 +388,7 @@ public class OsuParser { tokens[2] = tokens[2].replaceAll("^\"|\"$", ""); String ext = OsuParser.getExtension(tokens[2]); if (ext.equals("jpg") || ext.equals("png")) - osu.bg = getDBString(file.getParent() + File.separator + tokens[2]); + osu.bg = getDBString(tokens[2]); break; case "2": // break periods try { @@ -623,7 +649,10 @@ public class OsuParser { * Returns the name of the current file being parsed, or null if none. */ public static String getCurrentFileName() { - return (currentFile != null) ? currentFile.getName() : null; + if (updatingDatabase) + return ""; + else + return (currentFile != null) ? currentFile.getName() : null; } /** @@ -637,13 +666,18 @@ public class OsuParser { return currentDirectoryIndex * 100 / totalDirectories; } + /** + * Returns whether or not the beatmap database is currently being updated. + */ + public static boolean isUpdatingDatabase() { return updatingDatabase; } + /** * Returns the String object in the database for the given String. * If none, insert the String into the database and return the original String. * @param s the string to retrieve * @return the string object */ - private static String getDBString(String s) { + public static String getDBString(String s) { String DBString = stringdb.get(s); if (DBString == null) { stringdb.put(s, s); diff --git a/src/itdelatrisu/opsu/OsuTimingPoint.java b/src/itdelatrisu/opsu/OsuTimingPoint.java index eedca9c6..be99b486 100644 --- a/src/itdelatrisu/opsu/OsuTimingPoint.java +++ b/src/itdelatrisu/opsu/OsuTimingPoint.java @@ -142,4 +142,16 @@ public class OsuTimingPoint { * @return true if active */ public boolean isKiaiTimeActive() { return kiai; } + + @Override + public String toString() { + if (inherited) + return String.format("%d,%d,%d,%d,%d,%d,%d,%d", + time, velocity, meter, (int) sampleType, + (int) sampleTypeCustom, sampleVolume, 1, (kiai) ? 1: 0); + else + return String.format("%d,%g,%d,%d,%d,%d,%d,%d", + time, beatLength, meter, (int) sampleType, + (int) sampleTypeCustom, sampleVolume, 0, (kiai) ? 1: 0); + } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index a576c31e..42031292 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -587,7 +587,7 @@ public class Utils { text = "Unpacking new beatmaps..."; progress = OszUnpacker.getUnpackerProgress(); } else if ((file = OsuParser.getCurrentFileName()) != null) { - text = "Loading beatmaps..."; + text = (OsuParser.isUpdatingDatabase()) ? "Updating database..." : "Loading beatmaps..."; progress = OsuParser.getParserProgress(); } else if ((file = SoundController.getCurrentFileName()) != null) { text = "Loading sounds..."; diff --git a/src/itdelatrisu/opsu/db/DBController.java b/src/itdelatrisu/opsu/db/DBController.java new file mode 100644 index 00000000..f87bca6b --- /dev/null +++ b/src/itdelatrisu/opsu/db/DBController.java @@ -0,0 +1,53 @@ +/* + * 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 . + */ + +package itdelatrisu.opsu.db; + +import itdelatrisu.opsu.ErrorHandler; + +/** + * Database controller. + */ +public class DBController { + // This class should not be instantiated. + private DBController() {} + + /** + * Initializes all databases. + */ + public static void init() { + // load the sqlite-JDBC driver using the current class loader + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + ErrorHandler.error("Could not load sqlite-JDBC driver.", e, true); + } + + // initialize the databases + OsuDB.init(); + ScoreDB.init(); + } + + /** + * Closes all database connections. + */ + public static void closeConnections() { + OsuDB.closeConnection(); + ScoreDB.closeConnection(); + } +} diff --git a/src/itdelatrisu/opsu/db/OsuDB.java b/src/itdelatrisu/opsu/db/OsuDB.java new file mode 100644 index 00000000..bc36fa71 --- /dev/null +++ b/src/itdelatrisu/opsu/db/OsuDB.java @@ -0,0 +1,352 @@ +/* + * 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 . + */ + +package itdelatrisu.opsu.db; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.OsuFile; +import itdelatrisu.opsu.OsuParser; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Handles connections and queries with the cached beatmap database. + */ +public class OsuDB { + /** + * Current database version. + * This value should be changed whenever the database format changes. + */ + private static final String DATABASE_VERSION = "2014-03-04"; + + /** Database connection. */ + private static Connection connection; + + /** Query statements. */ + private static PreparedStatement insertStmt, selectStmt, lastModStmt, deleteStmt; + + // This class should not be instantiated. + private OsuDB() {} + + /** + * Initializes the database connection. + */ + public static void init() { + // create a database connection + try { + connection = DriverManager.getConnection(String.format("jdbc:sqlite:%s", Options.OSU_DB.getPath())); + } catch (SQLException e) { + // if the error message is "out of memory", it probably means no database file is found + ErrorHandler.error("Could not connect to beatmap database.", e, true); + } + + // create the database + createDatabase(); + + // check the database version + checkVersion(); + + // prepare sql statements + try { + insertStmt = connection.prepareStatement( + "INSERT INTO beatmaps VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ); + lastModStmt = connection.prepareStatement("SELECT dir, file, lastModified FROM beatmaps"); + selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?"); + deleteStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?"); + } catch (SQLException e) { + ErrorHandler.error("Failed to prepare beatmap 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 beatmaps (" + + "dir TEXT, file TEXT, lastModified INTEGER, " + + "MID INTEGER, MSID INTEGER, " + + "title TEXT, titleUnicode TEXT, artist TEXT, artistUnicode TEXT, " + + "creator TEXT, version TEXT, source TEXT, tags TEXT, " + + "circles INTEGER, sliders INTEGER, spinners INTEGER, " + + "hp REAL, cs REAL, od REAL, ar REAL, sliderMultiplier REAL, sliderTickRate REAL, " + + "bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " + + "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " + + "mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " + + "bg TEXT, timingPoints TEXT, breaks TEXT, combo TEXT" + + "); " + + "CREATE TABLE IF NOT EXISTS info (" + + "key TEXT NOT NULL UNIQUE, value TEXT" + + ")"; + stmt.executeUpdate(sql); + } catch (SQLException e) { + ErrorHandler.error("Could not create beatmap database.", e, true); + } + } + + /** + * Checks the stored table version, clears the beatmap database if different + * from the current version, then updates the version field. + */ + private static void checkVersion() { + try (Statement stmt = connection.createStatement()) { + // get the stored version + String sql = "SELECT value FROM info WHERE key = 'version'"; + ResultSet rs = stmt.executeQuery(sql); + String version = (rs.next()) ? rs.getString(1) : ""; + rs.close(); + + // if different from current version, clear the database + if (!version.equals(DATABASE_VERSION)) + clearDatabase(); + + // update version + PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)"); + ps.setString(1, DATABASE_VERSION); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + ErrorHandler.error("Beatmap database version checks failed.", e, true); + } + } + + /** + * Clears the database. + */ + public static void clearDatabase() { + // drop the table, then recreate it + try (Statement stmt = connection.createStatement()) { + String sql = "DROP TABLE beatmaps"; + stmt.executeUpdate(sql); + } catch (SQLException e) { + ErrorHandler.error("Could not drop beatmap database.", e, true); + } + createDatabase(); + } + + /** + * Adds the OsuFile to the database. + * @param osu the OsuFile object + */ + public static void insert(OsuFile osu) { + try { + setStatementFields(insertStmt, osu); + insertStmt.executeUpdate(); + } catch (SQLException e) { + ErrorHandler.error("Failed to add beatmap to database.", e, true); + } + } + + /** + * Adds the OsuFiles to the database in a batch. + * @param batch a list of OsuFile objects + */ + public static void insert(List batch) { + try { + // turn off auto-commit mode + boolean autoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + + // batch insert + for (OsuFile osu : batch) { + setStatementFields(insertStmt, osu); + insertStmt.addBatch(); + } + insertStmt.executeBatch(); + connection.commit(); + + // restore previous auto-commit mode + connection.setAutoCommit(autoCommit); + } catch (SQLException e) { + ErrorHandler.error("Failed to add beatmaps to database.", e, true); + } + } + + /** + * Sets all statement fields using a given OsuFile object. + * @param stmt the statement to set fields for + * @param osu the OsuFile + * @throws SQLException + */ + private static void setStatementFields(PreparedStatement stmt, OsuFile osu) + throws SQLException { + stmt.setString(1, osu.getFile().getParentFile().getName()); + stmt.setString(2, osu.getFile().getName()); + stmt.setLong(3, osu.getFile().lastModified()); + stmt.setInt(4, osu.beatmapID); + stmt.setInt(5, osu.beatmapSetID); + stmt.setString(6, osu.title); + stmt.setString(7, osu.titleUnicode); + stmt.setString(8, osu.artist); + stmt.setString(9, osu.artistUnicode); + stmt.setString(10, osu.creator); + stmt.setString(11, osu.version); + stmt.setString(12, osu.source); + stmt.setString(13, osu.tags); + stmt.setInt(14, osu.hitObjectCircle); + stmt.setInt(15, osu.hitObjectSlider); + stmt.setInt(16, osu.hitObjectSpinner); + stmt.setFloat(17, osu.HPDrainRate); + stmt.setFloat(18, osu.circleSize); + stmt.setFloat(19, osu.overallDifficulty); + stmt.setFloat(20, osu.approachRate); + stmt.setFloat(21, osu.sliderMultiplier); + stmt.setFloat(22, osu.sliderTickRate); + stmt.setInt(23, osu.bpmMin); + stmt.setInt(24, osu.bpmMax); + stmt.setInt(25, osu.endTime); + stmt.setString(26, osu.audioFilename.getName()); + stmt.setInt(27, osu.audioLeadIn); + stmt.setInt(28, osu.previewTime); + stmt.setByte(29, osu.countdown); + stmt.setString(30, osu.sampleSet); + stmt.setFloat(31, osu.stackLeniency); + stmt.setByte(32, osu.mode); + stmt.setBoolean(33, osu.letterboxInBreaks); + stmt.setBoolean(34, osu.widescreenStoryboard); + stmt.setBoolean(35, osu.epilepsyWarning); + stmt.setString(36, osu.bg); + stmt.setString(37, osu.timingPointsToString()); + stmt.setString(38, osu.breaksToString()); + stmt.setString(39, osu.comboToString()); + } + + /** + * Returns an OsuFile from the database, or null if any error occurred. + * @param dir the directory + * @param file the file + */ + public static OsuFile getOsuFile(File dir, File file) { + try { + OsuFile osu = new OsuFile(file); + selectStmt.setString(1, dir.getName()); + selectStmt.setString(2, file.getName()); + ResultSet rs = selectStmt.executeQuery(); + while (rs.next()) { + osu.beatmapID = rs.getInt(4); + osu.beatmapSetID = rs.getInt(5); + osu.title = OsuParser.getDBString(rs.getString(6)); + osu.titleUnicode = OsuParser.getDBString(rs.getString(7)); + osu.artist = OsuParser.getDBString(rs.getString(8)); + osu.artistUnicode = OsuParser.getDBString(rs.getString(9)); + osu.creator = OsuParser.getDBString(rs.getString(10)); + osu.version = OsuParser.getDBString(rs.getString(11)); + osu.source = OsuParser.getDBString(rs.getString(12)); + osu.tags = OsuParser.getDBString(rs.getString(13)); + osu.hitObjectCircle = rs.getInt(14); + osu.hitObjectSlider = rs.getInt(15); + osu.hitObjectSpinner = rs.getInt(16); + osu.HPDrainRate = rs.getFloat(17); + osu.circleSize = rs.getFloat(18); + osu.overallDifficulty = rs.getFloat(19); + osu.approachRate = rs.getFloat(20); + osu.sliderMultiplier = rs.getFloat(21); + osu.sliderTickRate = rs.getFloat(22); + osu.bpmMin = rs.getInt(23); + osu.bpmMax = rs.getInt(24); + osu.endTime = rs.getInt(25); + osu.audioFilename = new File(dir, OsuParser.getDBString(rs.getString(26))); + osu.audioLeadIn = rs.getInt(27); + osu.previewTime = rs.getInt(28); + osu.countdown = rs.getByte(29); + osu.sampleSet = OsuParser.getDBString(rs.getString(30)); + osu.stackLeniency = rs.getFloat(31); + osu.mode = rs.getByte(32); + osu.letterboxInBreaks = rs.getBoolean(33); + osu.widescreenStoryboard = rs.getBoolean(34); + osu.epilepsyWarning = rs.getBoolean(35); + osu.bg = OsuParser.getDBString(rs.getString(36)); + osu.timingPointsFromString(rs.getString(37)); + osu.breaksFromString(rs.getString(38)); + osu.comboFromString(rs.getString(39)); + } + rs.close(); + return osu; + } catch (SQLException e) { + ErrorHandler.error("Failed to get OsuFile from database.", e, true); + return null; + } + } + + /** + * Returns a map of file paths ({dir}/{file}) to last modified times, or + * null if any error occurred. + */ + public static Map getLastModifiedMap() { + try { + Map map = new HashMap(); + ResultSet rs = lastModStmt.executeQuery(); + while (rs.next()) { + String path = String.format("%s/%s", rs.getString(1), rs.getString(2)); + long lastModified = rs.getLong(3); + map.put(path, lastModified); + } + rs.close(); + return map; + } catch (SQLException e) { + ErrorHandler.error("Failed to get last modified map from database.", e, true); + return null; + } + } + + /** + * Deletes the entry from the database. + * @param dir the directory + * @param file the file + */ + public static void delete(String dir, String file) { + try { + deleteStmt.setString(1, dir); + deleteStmt.setString(2, file); + deleteStmt.executeUpdate(); + } catch (SQLException e) { + ErrorHandler.error("Failed to delete entry from database.", e, true); + } + } + + /** + * Closes the connection to the database. + */ + public static void closeConnection() { + if (connection != null) { + try { + insertStmt.close(); + lastModStmt.close(); + selectStmt.close(); + deleteStmt.close(); + connection.close(); + } catch (SQLException e) { + ErrorHandler.error("Failed to close beatmap database.", e, true); + } + } + } +} diff --git a/src/itdelatrisu/opsu/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java similarity index 90% rename from src/itdelatrisu/opsu/ScoreDB.java rename to src/itdelatrisu/opsu/db/ScoreDB.java index b7fc6aca..7b40c8e2 100644 --- a/src/itdelatrisu/opsu/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -16,7 +16,12 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu; +package itdelatrisu.opsu.db; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.OsuFile; +import itdelatrisu.opsu.ScoreData; import java.sql.Connection; import java.sql.DriverManager; @@ -51,16 +56,9 @@ public class ScoreDB { private ScoreDB() {} /** - * Initializes the score database connection. + * Initializes the database connection. */ public static void init() { - // load the sqlite-JDBC driver using the current class loader - try { - Class.forName("org.sqlite.JDBC"); - } catch (ClassNotFoundException e) { - ErrorHandler.error("Could not load sqlite-JDBC driver.", e, true); - } - // create a database connection try { connection = DriverManager.getConnection(String.format("jdbc:sqlite:%s", Options.SCORE_DB.getPath())); @@ -96,12 +94,12 @@ public class ScoreDB { "geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ?" ); } catch (SQLException e) { - ErrorHandler.error("Failed to prepare score insertion statement.", e, true); + ErrorHandler.error("Failed to prepare score statements.", e, true); } } /** - * Creates the score database, if it does not exist. + * Creates the database, if it does not exist. */ private static void createDatabase() { try (Statement stmt = connection.createStatement()) { @@ -265,7 +263,7 @@ public class ScoreDB { } /** - * Closes the connection to the score database. + * Closes the connection to the database. */ public static void closeConnection() { if (connection != null) { @@ -279,19 +277,4 @@ public class ScoreDB { } } } - - /** - * Prints the entire database (for debugging purposes). - */ - protected static void printDatabase() { - try ( - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM scores ORDER BY timestamp ASC"); - ) { - while (rs.next()) - System.out.println(new ScoreData(rs)); - } catch (SQLException e) { - ErrorHandler.error(null, e, false); - } - } } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 17a6a5c8..697ac1f3 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -28,13 +28,13 @@ import itdelatrisu.opsu.Options; import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.OsuTimingPoint; -import itdelatrisu.opsu.ScoreDB; import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.HitObject; import itdelatrisu.opsu.objects.Slider; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 12ab6e21..0fc5af52 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -30,7 +30,6 @@ import itdelatrisu.opsu.OsuGroupList; import itdelatrisu.opsu.OsuGroupNode; import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.OszUnpacker; -import itdelatrisu.opsu.ScoreDB; import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.SongSort; import itdelatrisu.opsu.Utils; @@ -39,6 +38,8 @@ import itdelatrisu.opsu.audio.MultiClip; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.db.OsuDB; +import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import java.io.File; @@ -1034,6 +1035,9 @@ public class SongMenu extends BasicGameState { reloadThread = new Thread() { @Override public void run() { + // clear the beatmap cache + OsuDB.clearDatabase(); + // invoke unpacker and parser File beatmapDir = Options.getBeatmapDir(); OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);