diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java
index bdf3747a..a099a283 100644
--- a/src/itdelatrisu/opsu/GameData.java
+++ b/src/itdelatrisu/opsu/GameData.java
@@ -18,7 +18,6 @@
package itdelatrisu.opsu;
-import itdelatrisu.opsu.Scores.ScoreData;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java
index e7e21a8e..0220f9aa 100644
--- a/src/itdelatrisu/opsu/Opsu.java
+++ b/src/itdelatrisu/opsu/Opsu.java
@@ -138,7 +138,7 @@ public class Opsu extends StateBasedGame {
Options.TMP_DIR.deleteOnExit();
// initialize score database
- Scores.init();
+ ScoreDB.init();
// start the game
try {
@@ -202,7 +202,7 @@ public class Opsu extends StateBasedGame {
*/
public static void exit() {
// close scores database
- Scores.closeConnection();
+ ScoreDB.closeConnection();
// close server socket
if (SERVER_SOCKET != null) {
diff --git a/src/itdelatrisu/opsu/ScoreDB.java b/src/itdelatrisu/opsu/ScoreDB.java
new file mode 100644
index 00000000..0adc3bf1
--- /dev/null
+++ b/src/itdelatrisu/opsu/ScoreDB.java
@@ -0,0 +1,243 @@
+/*
+ * 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;
+
+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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Handles connections and queries with the scores database.
+ */
+public class ScoreDB {
+ /** Database connection. */
+ private static Connection connection;
+
+ /** Score insertion statement. */
+ private static PreparedStatement insertStmt;
+
+ /** Score select statement. */
+ private static PreparedStatement selectMapStmt, selectMapSetStmt;
+
+ // This class should not be instantiated.
+ private ScoreDB() {}
+
+ /**
+ * Initializes the score 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));
+ } 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 score database.", e, true);
+ }
+
+ // create the database
+ createDatabase();
+
+ // prepare sql statements
+ try {
+ insertStmt = connection.prepareStatement(
+ "INSERT 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"
+ );
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to prepare score insertion statement.", e, true);
+ }
+ }
+
+ /**
+ * Creates the score 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" +
+ ")";
+ stmt.executeUpdate(sql);
+ } catch (SQLException e) {
+ ErrorHandler.error("Could not create score database.", e, true);
+ }
+ }
+
+ /**
+ * Adds the game score to the database.
+ * @param data the GameData object
+ */
+ public static void addScore(ScoreData score) {
+ try {
+ insertStmt.setLong(1, score.timestamp);
+ insertStmt.setInt(2, score.MID);
+ insertStmt.setInt(3, score.MSID);
+ insertStmt.setString(4, score.title);
+ insertStmt.setString(5, score.artist);
+ insertStmt.setString(6, score.creator);
+ insertStmt.setString(7, score.version);
+ insertStmt.setInt(8, score.hit300);
+ insertStmt.setInt(9, score.hit100);
+ insertStmt.setInt(10, score.hit50);
+ insertStmt.setInt(11, score.geki);
+ insertStmt.setInt(12, score.katu);
+ insertStmt.setInt(13, score.miss);
+ insertStmt.setLong(14, score.score);
+ insertStmt.setInt(15, score.combo);
+ insertStmt.setBoolean(16, score.perfect);
+ insertStmt.setInt(17, score.mods);
+ insertStmt.executeUpdate();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to save score to database.", e, true);
+ }
+ }
+
+ /**
+ * Retrieves the game scores for an OsuFile map.
+ * @param osu the OsuFile
+ * @return all scores for the beatmap
+ */
+ public static ScoreData[] getMapScores(OsuFile osu) {
+ List list = new ArrayList();
+ try {
+ selectMapStmt.setInt(1, osu.beatmapID);
+ selectMapStmt.setString(2, osu.title);
+ selectMapStmt.setString(3, osu.artist);
+ selectMapStmt.setString(4, osu.creator);
+ selectMapStmt.setString(5, osu.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 an OsuFile map set.
+ * @param osu the OsuFile
+ * @return all scores for the beatmap set (Version, ScoreData[])
+ */
+ public static Map getMapSetScores(OsuFile osu) {
+ Map map = new HashMap();
+ try {
+ selectMapSetStmt.setInt(1, osu.beatmapSetID);
+ selectMapSetStmt.setString(2, osu.title);
+ selectMapSetStmt.setString(3, osu.artist);
+ selectMapSetStmt.setString(4, osu.creator);
+ ResultSet rs = selectMapSetStmt.executeQuery();
+
+ List 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();
+ }
+ 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 list) {
+ ScoreData[] scores = list.toArray(new ScoreData[list.size()]);
+ Arrays.sort(scores, Collections.reverseOrder());
+ return scores;
+ }
+
+ /**
+ * Closes the connection to the score database.
+ */
+ public static void closeConnection() {
+ if (connection != null) {
+ try {
+ insertStmt.close();
+ selectMapStmt.close();
+ selectMapSetStmt.close();
+ connection.close();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to close score database.", e, true);
+ }
+ }
+ }
+
+ /**
+ * 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/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java
new file mode 100644
index 00000000..4fbdfa93
--- /dev/null
+++ b/src/itdelatrisu/opsu/ScoreData.java
@@ -0,0 +1,291 @@
+/*
+ * 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;
+
+import itdelatrisu.opsu.GameData.Grade;
+import itdelatrisu.opsu.states.SongMenu;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Graphics;
+import org.newdawn.slick.Image;
+
+/**
+ * Class encapsulating and drawing all score data.
+ */
+public class ScoreData implements Comparable {
+ /** The time when the score was achieved (Unix time). */
+ public long timestamp;
+
+ /** The beatmap and beatmap set IDs. */
+ public int MID, MSID;
+
+ /** Beatmap metadata. */
+ public String title, artist, creator, version;
+
+ /** Hit counts. */
+ public int hit300, hit100, hit50, geki, katu, miss;
+
+ /** The score. */
+ public long score;
+
+ /** The max combo. */
+ public int combo;
+
+ /** Whether or not a full combo was achieved. */
+ public boolean perfect;
+
+ /** Game mod bitmask. */
+ public int mods;
+
+ /** The grade. */
+ private Grade grade;
+
+ /** The score percent. */
+ private float scorePercent = -1f;
+
+ /** Drawing values. */
+ private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
+
+ /** Container dimensions. */
+ private static int containerWidth, containerHeight;
+
+ /** Button background colors. */
+ private static final Color
+ BG_NORMAL = new Color(0, 0, 0, 0.25f),
+ BG_FOCUS = new Color(0, 0, 0, 0.75f);
+
+ /**
+ * Initializes the base coordinates for drawing.
+ * @param width the container width
+ * @param height the container height
+ */
+ public static void init(int width, int height) {
+ containerWidth = width;
+ containerHeight = height;
+
+ baseX = width * 0.01f;
+ baseY = height * 0.16f;
+ buttonWidth = width * 0.4f;
+ float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
+ buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f);
+ buttonOffset = buttonHeight + gradeHeight / 10f;
+ }
+
+ /**
+ * Returns true if the coordinates are within the bounds of the
+ * button at the given index.
+ * @param cx the x coordinate
+ * @param cy the y coordinate
+ * @param index the index (to offset the button from the topmost button)
+ */
+ public static boolean buttonContains(float cx, float cy, int index) {
+ float y = baseY + (index * buttonOffset);
+ return ((cx >= 0 && cx < baseX + buttonWidth) &&
+ (cy > y && cy < y + buttonHeight));
+ }
+
+ /**
+ * Returns true if the coordinates are within the bounds of the
+ * score button area.
+ * @param cx the x coordinate
+ * @param cy the y coordinate
+ */
+ public static boolean areaContains(float cx, float cy) {
+ return ((cx >= 0 && cx < baseX + buttonWidth) &&
+ (cy > baseY && cy < baseY + buttonOffset * SongMenu.MAX_SCORE_BUTTONS));
+ }
+
+ /**
+ * Draws the scroll bar for the score buttons.
+ * @param g the graphics context
+ * @param index the start button index
+ * @param total the total number of buttons
+ */
+ public static void drawScrollbar(Graphics g, int index, int total) {
+ float scorebarWidth = containerWidth * 0.00347f;
+ float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
+ float scorebarHeight = containerHeight * heightRatio;
+ float heightDiff = buttonHeight + buttonOffset * (SongMenu.MAX_SCORE_BUTTONS - 1) - scorebarHeight;
+ float offsetY = heightDiff * ((float) index / (total - SongMenu.MAX_SCORE_BUTTONS));
+ g.setColor(Color.white);
+ g.fillRect(0, baseY + offsetY, scorebarWidth, scorebarHeight);
+ }
+
+ /**
+ * Empty constructor.
+ */
+ public ScoreData() {}
+
+ /**
+ * Constructor.
+ * @param rs the ResultSet to read from (at the current cursor position)
+ * @throws SQLException
+ */
+ public ScoreData(ResultSet rs) throws SQLException {
+ this.timestamp = rs.getLong(1);
+ this.MID = rs.getInt(2);
+ this.MSID = rs.getInt(3);
+ this.title = rs.getString(4);
+ this.artist = rs.getString(5);
+ this.creator = rs.getString(6);
+ this.version = rs.getString(7);
+ this.hit300 = rs.getInt(8);
+ this.hit100 = rs.getInt(9);
+ this.hit50 = rs.getInt(10);
+ this.geki = rs.getInt(11);
+ this.katu = rs.getInt(12);
+ this.miss = rs.getInt(13);
+ this.score = rs.getLong(14);
+ this.combo = rs.getInt(15);
+ this.perfect = rs.getBoolean(16);
+ this.mods = rs.getInt(17);
+ }
+
+ /**
+ * Returns the timestamp as a string.
+ */
+ public String getTimeString() {
+ return new SimpleDateFormat("M/d/yyyy h:mm:ss a").format(new Date(timestamp * 1000L));
+ }
+
+ /**
+ * Returns the raw score percentage based on score data.
+ * @see GameData#getScorePercent(int, int, int, int)
+ */
+ private float getScorePercent() {
+ if (scorePercent < 0f)
+ scorePercent = GameData.getScorePercent(hit300, hit100, hit50, miss);
+ return scorePercent;
+ }
+
+ /**
+ * Returns letter grade based on score data,
+ * or Grade.NULL if no objects have been processed.
+ * @see GameData#getGrade(int, int, int, int)
+ */
+ public Grade getGrade() {
+ if (grade == null)
+ grade = GameData.getGrade(hit300, hit100, hit50, miss);
+ return grade;
+ }
+
+ /**
+ * Draws the score data as a rectangular button.
+ * @param g the graphics context
+ * @param index the index (to offset the button from the topmost button)
+ * @param rank the score rank
+ * @param prevScore the previous (lower) score, or -1 if none
+ * @param focus whether the button is focused
+ */
+ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) {
+ Image img = getGrade().getMenuImage();
+ float y = baseY + index * (buttonOffset);
+ float textX = baseX + buttonWidth * 0.24f, edgeX = baseX + buttonWidth * 0.98f;
+ float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f;
+
+ // rectangle outline
+ g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
+ g.fillRect(baseX, y, buttonWidth, buttonHeight);
+
+ // rank
+ if (focus) {
+ Utils.FONT_LARGE.drawString(
+ baseX + buttonWidth * 0.04f,
+ y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
+ Integer.toString(rank + 1), Color.white
+ );
+ }
+
+ // grade image
+ img.drawCentered(baseX + buttonWidth * 0.15f, y + buttonHeight / 2f);
+
+ // score
+ float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
+ Utils.FONT_MEDIUM.drawString(
+ textX, y + textOffset,
+ String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
+ Color.white
+ );
+
+ // hit counts (custom: osu! shows user instead, above score)
+ Utils.FONT_SMALL.drawString(
+ textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
+ String.format("300:%d 100:%d 50:%d Miss:%d", hit300, hit100, hit50, miss),
+ Color.white
+ );
+
+ // mods
+ StringBuilder sb = new StringBuilder();
+ for (GameMod mod : GameMod.values()) {
+ if ((mod.getBit() & mods) > 0) {
+ sb.append(mod.getAbbreviation());
+ sb.append(',');
+ }
+ }
+ if (sb.length() > 0) {
+ sb.setLength(sb.length() - 1);
+ String modString = sb.toString();
+ Utils.FONT_DEFAULT.drawString(
+ edgeX - Utils.FONT_DEFAULT.getWidth(modString),
+ y + marginY, modString, Color.white
+ );
+ }
+
+ // accuracy
+ String accuracy = String.format("%.2f%%", getScorePercent());
+ Utils.FONT_DEFAULT.drawString(
+ edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
+ y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
+ accuracy, Color.white
+ );
+
+ // score difference
+ String diff = (prevScore < 0 || score < prevScore) ?
+ "-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
+ Utils.FONT_DEFAULT.drawString(
+ edgeX - Utils.FONT_DEFAULT.getWidth(diff),
+ y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
+ diff, Color.white
+ );
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s | ID: (%d, %d) | %s - %s [%s] (by %s) | " +
+ "Hits: (%d, %d, %d, %d, %d, %d) | Score: %d (%d combo%s) | Mods: %d",
+ getTimeString(), MID, MSID, artist, title, version, creator,
+ hit300, hit100, hit50, geki, katu, miss, score, combo, perfect ? ", FC" : "", mods
+ );
+ }
+
+ @Override
+ public int compareTo(ScoreData that) {
+ if (this.score != that.score)
+ return Long.compare(this.score, that.score);
+ else
+ return Long.compare(this.timestamp, that.timestamp);
+ }
+}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/Scores.java b/src/itdelatrisu/opsu/Scores.java
deleted file mode 100644
index b2baf4bd..00000000
--- a/src/itdelatrisu/opsu/Scores.java
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- * 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;
-
-import itdelatrisu.opsu.GameData.Grade;
-import itdelatrisu.opsu.states.SongMenu;
-
-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.text.NumberFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.newdawn.slick.Color;
-import org.newdawn.slick.Graphics;
-import org.newdawn.slick.Image;
-
-/**
- * Handles game score data.
- */
-public class Scores {
- /** Class encapsulating all score data. */
- public static class ScoreData implements Comparable {
- /** The time when the score was achieved (Unix time). */
- public long timestamp;
-
- /** The beatmap and beatmap set IDs. */
- public int MID, MSID;
-
- /** Beatmap metadata. */
- public String title, artist, creator, version;
-
- /** Hit counts. */
- public int hit300, hit100, hit50, geki, katu, miss;
-
- /** The score. */
- public long score;
-
- /** The max combo. */
- public int combo;
-
- /** Whether or not a full combo was achieved. */
- public boolean perfect;
-
- /** Game mod bitmask. */
- public int mods;
-
- /** The grade. */
- private Grade grade;
-
- /** The score percent. */
- private float scorePercent = -1f;
-
- /** Drawing values. */
- private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
-
- /** Container dimensions. */
- private static int containerWidth, containerHeight;
-
- /** Button background colors. */
- private static final Color
- BG_NORMAL = new Color(0, 0, 0, 0.25f),
- BG_FOCUS = new Color(0, 0, 0, 0.75f);
-
- /**
- * Initializes the base coordinates for drawing.
- * @param width the container width
- * @param height the container height
- */
- public static void init(int width, int height) {
- containerWidth = width;
- containerHeight = height;
-
- baseX = width * 0.01f;
- baseY = height * 0.16f;
- buttonWidth = width * 0.4f;
- float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
- buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f);
- buttonOffset = buttonHeight + gradeHeight / 10f;
- }
-
- /**
- * Returns true if the coordinates are within the bounds of the
- * button at the given index.
- * @param cx the x coordinate
- * @param cy the y coordinate
- * @param index the index (to offset the button from the topmost button)
- */
- public static boolean buttonContains(float cx, float cy, int index) {
- float y = baseY + (index * buttonOffset);
- return ((cx >= 0 && cx < baseX + buttonWidth) &&
- (cy > y && cy < y + buttonHeight));
- }
-
- /**
- * Returns true if the coordinates are within the bounds of the
- * score button area.
- * @param cx the x coordinate
- * @param cy the y coordinate
- */
- public static boolean areaContains(float cx, float cy) {
- return ((cx >= 0 && cx < baseX + buttonWidth) &&
- (cy > baseY && cy < baseY + buttonOffset * SongMenu.MAX_SCORE_BUTTONS));
- }
-
- /**
- * Draws the scroll bar for the score buttons.
- * @param g the graphics context
- * @param index the start button index
- * @param total the total number of buttons
- */
- public static void drawScrollbar(Graphics g, int index, int total) {
- float scorebarWidth = containerWidth * 0.00347f;
- float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
- float scorebarHeight = containerHeight * heightRatio;
- float heightDiff = buttonHeight + buttonOffset * (SongMenu.MAX_SCORE_BUTTONS - 1) - scorebarHeight;
- float offsetY = heightDiff * ((float) index / (total - SongMenu.MAX_SCORE_BUTTONS));
- g.setColor(Color.white);
- g.fillRect(0, baseY + offsetY, scorebarWidth, scorebarHeight);
- }
-
- /**
- * Empty constructor.
- */
- public ScoreData() {}
-
- /**
- * Constructor.
- * @param rs the ResultSet to read from (at the current cursor position)
- * @throws SQLException
- */
- public ScoreData(ResultSet rs) throws SQLException {
- this.timestamp = rs.getLong(1);
- this.MID = rs.getInt(2);
- this.MSID = rs.getInt(3);
- this.title = rs.getString(4);
- this.artist = rs.getString(5);
- this.creator = rs.getString(6);
- this.version = rs.getString(7);
- this.hit300 = rs.getInt(8);
- this.hit100 = rs.getInt(9);
- this.hit50 = rs.getInt(10);
- this.geki = rs.getInt(11);
- this.katu = rs.getInt(12);
- this.miss = rs.getInt(13);
- this.score = rs.getLong(14);
- this.combo = rs.getInt(15);
- this.perfect = rs.getBoolean(16);
- this.mods = rs.getInt(17);
- }
-
- /**
- * Returns the timestamp as a string.
- */
- public String getTimeString() {
- return new SimpleDateFormat("M/d/yyyy h:mm:ss a").format(new Date(timestamp * 1000L));
- }
-
- /**
- * Returns the raw score percentage based on score data.
- * @see GameData#getScorePercent(int, int, int, int)
- */
- private float getScorePercent() {
- if (scorePercent < 0f)
- scorePercent = GameData.getScorePercent(hit300, hit100, hit50, miss);
- return scorePercent;
- }
-
- /**
- * Returns letter grade based on score data,
- * or Grade.NULL if no objects have been processed.
- * @see GameData#getGrade(int, int, int, int)
- */
- public Grade getGrade() {
- if (grade == null)
- grade = GameData.getGrade(hit300, hit100, hit50, miss);
- return grade;
- }
-
- /**
- * Draws the score data as a rectangular button.
- * @param g the graphics context
- * @param index the index (to offset the button from the topmost button)
- * @param rank the score rank
- * @param prevScore the previous (lower) score, or -1 if none
- * @param focus whether the button is focused
- */
- public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) {
- Image img = getGrade().getMenuImage();
- float y = baseY + index * (buttonOffset);
- float textX = baseX + buttonWidth * 0.24f, edgeX = baseX + buttonWidth * 0.98f;
- float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f;
-
- // rectangle outline
- g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
- g.fillRect(baseX, y, buttonWidth, buttonHeight);
-
- // rank
- if (focus) {
- Utils.FONT_LARGE.drawString(
- baseX + buttonWidth * 0.04f,
- y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
- Integer.toString(rank + 1), Color.white
- );
- }
-
- // grade image
- img.drawCentered(baseX + buttonWidth * 0.15f, y + buttonHeight / 2f);
-
- // score
- float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
- Utils.FONT_MEDIUM.drawString(
- textX, y + textOffset,
- String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
- Color.white
- );
-
- // hit counts (custom: osu! shows user instead, above score)
- Utils.FONT_SMALL.drawString(
- textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
- String.format("300:%d 100:%d 50:%d Miss:%d", hit300, hit100, hit50, miss),
- Color.white
- );
-
- // mods
- StringBuilder sb = new StringBuilder();
- for (GameMod mod : GameMod.values()) {
- if ((mod.getBit() & mods) > 0) {
- sb.append(mod.getAbbreviation());
- sb.append(',');
- }
- }
- if (sb.length() > 0) {
- sb.setLength(sb.length() - 1);
- String modString = sb.toString();
- Utils.FONT_DEFAULT.drawString(
- edgeX - Utils.FONT_DEFAULT.getWidth(modString),
- y + marginY, modString, Color.white
- );
- }
-
- // accuracy
- String accuracy = String.format("%.2f%%", getScorePercent());
- Utils.FONT_DEFAULT.drawString(
- edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
- y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
- accuracy, Color.white
- );
-
- // score difference
- String diff = (prevScore < 0 || score < prevScore) ?
- "-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
- Utils.FONT_DEFAULT.drawString(
- edgeX - Utils.FONT_DEFAULT.getWidth(diff),
- y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
- diff, Color.white
- );
- }
-
- @Override
- public String toString() {
- return String.format(
- "%s | ID: (%d, %d) | %s - %s [%s] (by %s) | " +
- "Hits: (%d, %d, %d, %d, %d, %d) | Score: %d (%d combo%s) | Mods: %d",
- getTimeString(), MID, MSID, artist, title, version, creator,
- hit300, hit100, hit50, geki, katu, miss, score, combo, perfect ? ", FC" : "", mods
- );
- }
-
- @Override
- public int compareTo(ScoreData that) {
- if (this.score != that.score)
- return Long.compare(this.score, that.score);
- else
- return Long.compare(this.timestamp, that.timestamp);
- }
- }
-
- /** Database connection. */
- private static Connection connection;
-
- /** Score insertion statement. */
- private static PreparedStatement insertStmt;
-
- /** Score select statement. */
- private static PreparedStatement selectMapStmt, selectMapSetStmt;
-
- // This class should not be instantiated.
- private Scores() {}
-
- /**
- * Initializes the score 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));
- } 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 score database.", e, true);
- }
-
- // create the database
- createDatabase();
-
- // prepare sql statements
- try {
- insertStmt = connection.prepareStatement(
- "INSERT 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"
- );
- } catch (SQLException e) {
- ErrorHandler.error("Failed to prepare score insertion statement.", e, true);
- }
- }
-
- /**
- * Creates the score 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" +
- ")";
- stmt.executeUpdate(sql);
- } catch (SQLException e) {
- ErrorHandler.error("Could not create score database.", e, true);
- }
- }
-
- /**
- * Adds the game score to the database.
- * @param data the GameData object
- */
- public static void addScore(ScoreData score) {
- try {
- insertStmt.setLong(1, score.timestamp);
- insertStmt.setInt(2, score.MID);
- insertStmt.setInt(3, score.MSID);
- insertStmt.setString(4, score.title);
- insertStmt.setString(5, score.artist);
- insertStmt.setString(6, score.creator);
- insertStmt.setString(7, score.version);
- insertStmt.setInt(8, score.hit300);
- insertStmt.setInt(9, score.hit100);
- insertStmt.setInt(10, score.hit50);
- insertStmt.setInt(11, score.geki);
- insertStmt.setInt(12, score.katu);
- insertStmt.setInt(13, score.miss);
- insertStmt.setLong(14, score.score);
- insertStmt.setInt(15, score.combo);
- insertStmt.setBoolean(16, score.perfect);
- insertStmt.setInt(17, score.mods);
- insertStmt.executeUpdate();
- } catch (SQLException e) {
- ErrorHandler.error("Failed to save score to database.", e, true);
- }
- }
-
- /**
- * Retrieves the game scores for an OsuFile map.
- * @param osu the OsuFile
- * @return all scores for the beatmap
- */
- public static ScoreData[] getMapScores(OsuFile osu) {
- List list = new ArrayList();
- try {
- selectMapStmt.setInt(1, osu.beatmapID);
- selectMapStmt.setString(2, osu.title);
- selectMapStmt.setString(3, osu.artist);
- selectMapStmt.setString(4, osu.creator);
- selectMapStmt.setString(5, osu.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 an OsuFile map set.
- * @param osu the OsuFile
- * @return all scores for the beatmap set (Version, ScoreData[])
- */
- public static Map getMapSetScores(OsuFile osu) {
- Map map = new HashMap();
- try {
- selectMapSetStmt.setInt(1, osu.beatmapSetID);
- selectMapSetStmt.setString(2, osu.title);
- selectMapSetStmt.setString(3, osu.artist);
- selectMapSetStmt.setString(4, osu.creator);
- ResultSet rs = selectMapSetStmt.executeQuery();
-
- List 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();
- }
- 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 list) {
- ScoreData[] scores = list.toArray(new ScoreData[list.size()]);
- Arrays.sort(scores, Collections.reverseOrder());
- return scores;
- }
-
- /**
- * Closes the connection to the score database.
- */
- public static void closeConnection() {
- if (connection != null) {
- try {
- insertStmt.close();
- selectMapStmt.close();
- selectMapSetStmt.close();
- connection.close();
- } catch (SQLException e) {
- ErrorHandler.error("Failed to close score database.", e, true);
- }
- }
- }
-
- /**
- * 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/Utils.java b/src/itdelatrisu/opsu/Utils.java
index f56f5299..7de530bd 100644
--- a/src/itdelatrisu/opsu/Utils.java
+++ b/src/itdelatrisu/opsu/Utils.java
@@ -18,7 +18,6 @@
package itdelatrisu.opsu;
-import itdelatrisu.opsu.Scores.ScoreData;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java
index b96a4327..ec9ab3e9 100644
--- a/src/itdelatrisu/opsu/states/Game.java
+++ b/src/itdelatrisu/opsu/states/Game.java
@@ -28,7 +28,7 @@ import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.OsuTimingPoint;
-import itdelatrisu.opsu.Scores;
+import itdelatrisu.opsu.ScoreDB;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
@@ -422,7 +422,7 @@ public class Game extends BasicGameState {
else { // go to ranking screen
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
if (!GameMod.AUTO.isActive())
- Scores.addScore(data.getScoreData(osu));
+ ScoreDB.addScore(data.getScoreData(osu));
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
return;
diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java
index 24ad54e5..4da0eb7d 100644
--- a/src/itdelatrisu/opsu/states/SongMenu.java
+++ b/src/itdelatrisu/opsu/states/SongMenu.java
@@ -30,8 +30,8 @@ import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker;
-import itdelatrisu.opsu.Scores;
-import itdelatrisu.opsu.Scores.ScoreData;
+import itdelatrisu.opsu.ScoreData;
+import itdelatrisu.opsu.ScoreDB;
import itdelatrisu.opsu.SongSort;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound;
@@ -796,7 +796,7 @@ public class SongMenu extends BasicGameState {
// reload scores
if (focusNode != null) {
- scoreMap = Scores.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex));
+ scoreMap = ScoreDB.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex));
focusScores = getScoreDataForNode(focusNode);
}
@@ -891,7 +891,7 @@ public class SongMenu extends BasicGameState {
Utils.loadGlyphs(osu);
// load scores
- scoreMap = Scores.getMapSetScores(osu);
+ scoreMap = ScoreDB.getMapSetScores(osu);
focusScores = getScoreDataForNode(focusNode);
startScore = 0;