Save game scores to an SQLite database. [Incomplete!]

Implemented basic features (mostly stable).  The remaining features are mostly graphical.
- Added package org.xerial.sqlite-jdbc.  All scores are saved to .opsu_scores.db on table `scores` after a game completes.
- Added "Scores" class to handle all game score data (including database connections).  The "Score" subclass encapsulates all database fields.
- Added "score viewing" constructor to GameData, for use only in the ranking screen.
- Draw the grade of the highest score next to expanded song buttons in the song menu.
- Added "bit" and "abbrev" fields to GameMod, used in storing/displaying scores.
- Hide the retry/exit buttons in the ranking screen when viewing a score.

Other changes:
- Removed "objectCount" field in GameData (no longer necessary).
- Removed "getID()" method in GameMod (no longer used).
- Moved most drawing in GameRanking state to GameData.
- Removed File parameter of "GameData.loadImages()" (leftover, no longer used).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-01-28 03:47:24 -05:00
parent f71b2c25f2
commit 0bd72e731a
13 changed files with 693 additions and 133 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
/Songs/
/.opsu.log
/.opsu.cfg
/.opsu_scores.db
# Eclipse
/.settings/

View File

@ -133,5 +133,10 @@
<artifactId>jlayer</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.8.7</version>
</dependency>
</dependencies>
</project>

View File

@ -74,10 +74,8 @@ public class Container extends AppGameContainer {
}
}
if (forceExit) {
Opsu.closeSocket();
System.exit(0);
}
if (forceExit)
Opsu.exit();
}
/**

View File

@ -18,12 +18,12 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.Scores.ScoreData;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@ -41,7 +41,7 @@ public class GameData {
public static final float HP_DRAIN_MULTIPLIER = 1 / 200f;
/** Letter grades. */
private enum Grade {
public enum Grade {
NULL (null, null),
SS (GameImage.RANKING_SS, GameImage.RANKING_SS_SMALL),
SSH (GameImage.RANKING_SSH, GameImage.RANKING_SSH_SMALL), // silver
@ -95,9 +95,6 @@ public class GameData {
/** Counts of each hit result so far. */
private int[] hitResultCount;
/** Total number of hit objects so far, not including Katu/Geki (for calculating grade). */
private int objectCount;
/** Total objects including slider hits/ticks (for determining Full Combo status). */
private int fullObjectCount;
@ -194,21 +191,57 @@ public class GameData {
/** Scorebar animation. */
private Animation scorebarColour;
/** The associated score data. */
private ScoreData scoreData;
/** Whether this object is used for gameplay (true) or score viewing (false). */
private boolean gameplay;
/** Container dimensions. */
private int width, height;
/**
* Constructor.
* Constructor for gameplay.
* @param width container width
* @param height container height
*/
public GameData(int width, int height) {
this.width = width;
this.height = height;
this.gameplay = true;
clear();
}
/**
* Constructor for score viewing.
* This will initialize all parameters and images needed for the
* {@link #drawRankingElements(Graphics, int, int)} method.
* @param s the ScoreData object
* @param width container width
* @param height container height
*/
public GameData(ScoreData s, int width, int height) {
this.width = width;
this.height = height;
this.gameplay = false;
this.scoreData = s;
this.score = s.score;
this.comboMax = s.combo;
this.fullObjectCount = (s.perfect) ? s.combo : -1;
this.hitResultCount = new int[HIT_MAX];
hitResultCount[HIT_300] = s.hit300;
hitResultCount[HIT_100] = s.hit100;
hitResultCount[HIT_50] = s.hit50;
hitResultCount[HIT_300G] = s.geki;
hitResultCount[HIT_300K] = 0;
hitResultCount[HIT_100K] = s.katu;
hitResultCount[HIT_MISS] = s.miss;
loadImages();
}
/**
* Clears all data and re-initializes object.
*/
@ -219,42 +252,44 @@ public class GameData {
healthDisplay = 100f;
hitResultCount = new int[HIT_MAX];
hitResultList = new LinkedList<OsuHitObjectResult>();
objectCount = 0;
fullObjectCount = 0;
combo = 0;
comboMax = 0;
comboEnd = 0;
comboBurstIndex = -1;
scoreData = null;
}
/**
* Loads all game score images.
* @param dir the image directory
*/
public void loadImages(File dir) {
// combo burst images
if (GameImage.COMBO_BURST.hasSkinImages() ||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
comboBurstImages = GameImage.COMBO_BURST.getImages();
else
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
// scorebar-colour animation
Image[] scorebar = GameImage.SCOREBAR_COLOUR.getImages();
scorebarColour = (scorebar != null) ? new Animation(scorebar, 60) : null;
// default symbol images
defaultSymbols = new Image[10];
defaultSymbols[0] = GameImage.DEFAULT_0.getImage();
defaultSymbols[1] = GameImage.DEFAULT_1.getImage();
defaultSymbols[2] = GameImage.DEFAULT_2.getImage();
defaultSymbols[3] = GameImage.DEFAULT_3.getImage();
defaultSymbols[4] = GameImage.DEFAULT_4.getImage();
defaultSymbols[5] = GameImage.DEFAULT_5.getImage();
defaultSymbols[6] = GameImage.DEFAULT_6.getImage();
defaultSymbols[7] = GameImage.DEFAULT_7.getImage();
defaultSymbols[8] = GameImage.DEFAULT_8.getImage();
defaultSymbols[9] = GameImage.DEFAULT_9.getImage();
public void loadImages() {
// gameplay-specific images
if (isGameplay()) {
// combo burst images
if (GameImage.COMBO_BURST.hasSkinImages() ||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
comboBurstImages = GameImage.COMBO_BURST.getImages();
else
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
// scorebar-colour animation
Image[] scorebar = GameImage.SCOREBAR_COLOUR.getImages();
scorebarColour = (scorebar != null) ? new Animation(scorebar, 60) : null;
// default symbol images
defaultSymbols = new Image[10];
defaultSymbols[0] = GameImage.DEFAULT_0.getImage();
defaultSymbols[1] = GameImage.DEFAULT_1.getImage();
defaultSymbols[2] = GameImage.DEFAULT_2.getImage();
defaultSymbols[3] = GameImage.DEFAULT_3.getImage();
defaultSymbols[4] = GameImage.DEFAULT_4.getImage();
defaultSymbols[5] = GameImage.DEFAULT_5.getImage();
defaultSymbols[6] = GameImage.DEFAULT_6.getImage();
defaultSymbols[7] = GameImage.DEFAULT_7.getImage();
defaultSymbols[8] = GameImage.DEFAULT_8.getImage();
defaultSymbols[9] = GameImage.DEFAULT_9.getImage();
}
// score symbol images
scoreSymbols = new HashMap<Character, Image>(14);
@ -471,12 +506,11 @@ public class GameData {
}
/**
* Draws ranking elements: score, results, ranking.
* Draws ranking elements: score, results, ranking, game mods.
* @param g the graphics context
* @param width the width of the container
* @param height the height of the container
* @param osu the OsuFile
*/
public void drawRankingElements(Graphics g, int width, int height) {
public void drawRankingElements(Graphics g, OsuFile osu) {
// grade
Grade grade = getGrade();
if (grade != Grade.NULL) {
@ -492,6 +526,14 @@ public class GameData {
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.fillRect(0, 0, width, rankingHeight);
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
float marginX = width * 0.01f, marginY = height * 0.01f;
Utils.FONT_LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", osu.getArtist(), osu.getTitle(), osu.version), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", osu.creator), Color.white);
Utils.FONT_MEDIUM.drawString(
marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
String.format("Played on %s.", scoreData.getTimeString()), Color.white);
// ranking panel
Image rankingPanel = GameImage.RANKING_PANEL.getImage();
@ -546,12 +588,23 @@ public class GameData {
(int) (rankingPanelWidth / 2f + numbersX), (int) numbersY, symbolTextScale, false);
// full combo
if (combo == fullObjectCount) {
if (comboMax == fullObjectCount) {
GameImage.RANKING_PERFECT.getImage().draw(
width * 0.08f,
(height * 0.99f) - GameImage.RANKING_PERFECT.getImage().getHeight()
);
}
// mod icons
int modWidth = GameMod.AUTO.getImage().getWidth();
float modX = (width * 0.98f) - modWidth;
int modCount = 0;
for (GameMod mod : GameMod.VALUES_REVERSED) {
if ((mod.getBit() & scoreData.mods) > 0) {
mod.getImage().draw(modX - (modCount * (modWidth / 2f)), height / 2f);
modCount++;
}
}
}
/**
@ -616,30 +669,50 @@ public class GameData {
public void changeScore(int value) { score += value; }
/**
* Returns score percentage (raw score only).
* Returns the raw score percentage.
* @param hit300 the number of 300s
* @param hit100 the number of 100s
* @param hit50 the number of 50s
* @param miss the number of misses
* @return the percentage
*/
public static float getScorePercent(int hit300, int hit100, int hit50, int miss) {
float percent = 0;
int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount > 0)
percent = (hit300 * 300 + hit100 * 100 + hit50 * 50) / (objectCount * 300f) * 100f;
return percent;
}
/**
* Returns the raw score percentage.
*/
private float getScorePercent() {
float percent = 0;
if (objectCount > 0)
percent = ((hitResultCount[HIT_50] * 50) + (hitResultCount[HIT_100] * 100)
+ (hitResultCount[HIT_300] * 300)) / (objectCount * 300f) * 100f;
return percent;
return getScorePercent(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS]
);
}
/**
* Returns letter grade based on score data,
* or Grade.NULL if no objects have been processed.
* @param hit300 the number of 300s
* @param hit100 the number of 100s
* @param hit50 the number of 50s
* @param miss the number of misses
* @return the current Grade
*/
private Grade getGrade() {
public static Grade getGrade(int hit300, int hit100, int hit50, int miss) {
int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount < 1) // avoid division by zero
return Grade.NULL;
// TODO: silvers
float percent = getScorePercent();
float hit300ratio = hitResultCount[HIT_300] * 100f / objectCount;
float hit50ratio = hitResultCount[HIT_50] * 100f / objectCount;
boolean noMiss = (hitResultCount[HIT_MISS] == 0);
float percent = getScorePercent(hit300, hit100, hit50, miss);
float hit300ratio = hit300 * 100f / objectCount;
float hit50ratio = hit50 * 100f / objectCount;
boolean noMiss = (miss == 0);
if (percent >= 100f)
return Grade.SS;
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
@ -654,6 +727,17 @@ public class GameData {
return Grade.D;
}
/**
* Returns letter grade based on score data,
* or Grade.NULL if no objects have been processed.
*/
private Grade getGrade() {
return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS]
);
}
/**
* Updates the score, health, and combo burst displays based on a delta value.
* @param delta the delta interval since the last call
@ -792,25 +876,21 @@ public class GameData {
perfectHit = true;
hitValue = 300;
changeHealth(5f);
objectCount++;
break;
case HIT_100:
hitValue = 100;
changeHealth(2f);
comboEnd |= 1;
objectCount++;
break;
case HIT_50:
hitValue = 50;
comboEnd |= 2;
objectCount++;
break;
case HIT_MISS:
hitValue = 0;
changeHealth(-10f);
comboEnd |= 2;
resetComboStreak();
objectCount++;
break;
default:
return;
@ -864,4 +944,47 @@ public class GameData {
else
hitResultList.add(new OsuHitObjectResult(time, result, x, y, color));
}
/**
* Returns a ScoreData object encapsulating all game data.
* If score data already exists, the existing object will be returned
* (i.e. this will not overwrite existing data).
* @param osu the OsuFile
* @return the ScoreData object
*/
public ScoreData getScoreData(OsuFile osu) {
if (scoreData != null)
return scoreData;
scoreData = new ScoreData();
scoreData.timestamp = System.currentTimeMillis() / 1000L;
scoreData.MID = osu.beatmapID;
scoreData.MSID = osu.beatmapSetID;
scoreData.title = osu.title;
scoreData.artist = osu.artist;
scoreData.creator = osu.creator;
scoreData.version = osu.version;
scoreData.hit300 = hitResultCount[HIT_300];
scoreData.hit100 = hitResultCount[HIT_100];
scoreData.hit50 = hitResultCount[HIT_50];
scoreData.geki = hitResultCount[HIT_300G];
scoreData.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K];
scoreData.miss = hitResultCount[HIT_MISS];
scoreData.score = score;
scoreData.combo = comboMax;
scoreData.perfect = (comboMax == fullObjectCount);
int mods = 0;
for (GameMod mod : GameMod.values()) {
if (mod.isActive())
mods |= mod.getBit();
}
scoreData.mods = mods;
return scoreData;
}
/**
* Returns whether or not this object is used for gameplay.
* @return true if gameplay, false if score viewing
*/
public boolean isGameplay() { return gameplay; }
}

View File

@ -164,7 +164,7 @@ public enum GameImage {
SPINNER_CLEAR ("spinner-clear", "png"),
SPINNER_OSU ("spinner-osu", "png"),
// Game Score
// Score Data
COMBO_BURST ("comboburst", "comboburst-%d", "png"),
SCOREBAR_BG ("scorebar-bg", "png") {
@Override

View File

@ -28,16 +28,16 @@ import org.newdawn.slick.Input;
* Game mods.
*/
public enum GameMod {
EASY (0, GameImage.MOD_EASY, Input.KEY_Q, 0.5f),
NO_FAIL (1, GameImage.MOD_NO_FAIL, Input.KEY_W, 0.5f),
HARD_ROCK (2, GameImage.MOD_HARD_ROCK, Input.KEY_A, 1.06f),
SUDDEN_DEATH (3, GameImage.MOD_SUDDEN_DEATH, Input.KEY_S),
SPUN_OUT (4, GameImage.MOD_SPUN_OUT, Input.KEY_V, 0.9f),
AUTO (5, GameImage.MOD_AUTO, Input.KEY_B);
// HALF_TIME (, GameImage.MOD_HALF_TIME, Input.KEY_E, 0.3f),
// DOUBLE_TIME (, GameImage.MOD_DOUBLE_TIME, Input.KEY_D, 1.12f),
// HIDDEN (, GameImage.MOD_HIDDEN, Input.KEY_F, 1.06f),
// FLASHLIGHT (, GameImage.MOD_FLASHLIGHT, Input.KEY_G, 1.12f);
EASY (0, GameImage.MOD_EASY, "EZ", 2, Input.KEY_Q, 0.5f),
NO_FAIL (1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f),
HARD_ROCK (2, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f),
SUDDEN_DEATH (3, GameImage.MOD_SUDDEN_DEATH, "SD", 32, Input.KEY_S),
SPUN_OUT (4, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f),
AUTO (5, GameImage.MOD_AUTO, "", 2048, Input.KEY_B);
// HALF_TIME (, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f),
// DOUBLE_TIME (, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f),
// HIDDEN (, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f),
// FLASHLIGHT (, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f);
/** The ID of the mod (used for positioning). */
private int id;
@ -45,7 +45,16 @@ public enum GameMod {
/** The file name of the mod image. */
private GameImage image;
/** The shortcut key associated with the mod. */
/** The abbreviation for the mod. */
private String abbrev;
/** Bit value associated with the mod. */
private int bit;
/**
* The shortcut key associated with the mod.
* See the osu! API: https://github.com/peppy/osu-api/wiki#mods
*/
private int key;
/** The score multiplier. */
@ -71,11 +80,15 @@ public enum GameMod {
* Constructor.
* @param id the ID of the mod (for positioning).
* @param image the GameImage
* @param abbrev the two-letter abbreviation
* @param bit the bit
* @param key the shortcut key
*/
GameMod(int id, GameImage image, int key) {
GameMod(int id, GameImage image, String abbrev, int bit, int key) {
this.id = id;
this.image = image;
this.abbrev = abbrev;
this.bit = bit;
this.key = key;
this.multiplier = 1f;
}
@ -84,12 +97,16 @@ public enum GameMod {
* Constructor.
* @param id the ID of the mod (for positioning).
* @param image the GameImage
* @param abbrev the two-letter abbreviation
* @param bit the bit
* @param key the shortcut key
* @param multiplier the score multiplier
*/
GameMod(int id, GameImage image, int key, float multiplier) {
GameMod(int id, GameImage image, String abbrev, int bit, int key, float multiplier) {
this.id = id;
this.image = image;
this.abbrev = abbrev;
this.bit = bit;
this.key = key;
this.multiplier = multiplier;
}
@ -113,6 +130,18 @@ public enum GameMod {
this.button.setHoverScale(1.15f);
}
/**
* Returns the abbreviated name of the mod.
* @return the two-letter abbreviation
*/
public String getAbbreviation() { return abbrev; }
/**
* Returns the bit associated with the mod.
* @return the bit
*/
public int getBit() { return bit; }
/**
* Returns the shortcut key for the mod.
* @return the key
@ -173,12 +202,6 @@ public enum GameMod {
*/
public Image getImage() { return image.getImage(); }
/**
* Returns the mod ID.
* @return the mod ID
*/
public int getID() { return id; }
/**
* Draws the game mod.
*/

View File

@ -137,6 +137,9 @@ public class Opsu extends StateBasedGame {
}
Options.TMP_DIR.deleteOnExit();
// initialize score database
Scores.init();
// start the game
try {
// loop until force exit
@ -161,8 +164,7 @@ public class Opsu extends StateBasedGame {
ErrorHandler.error("Error while creating game container.", e, true);
}
// close server socket
closeSocket();
Opsu.exit();
}
@Override
@ -188,9 +190,13 @@ public class Opsu extends StateBasedGame {
}
/**
* Closes the server socket.
* Closes all resources and exits the application.
*/
public static void closeSocket() {
public static void exit() {
// close scores database
Scores.closeConnection();
// close server socket
if (SERVER_SOCKET != null) {
try {
SERVER_SOCKET.close();
@ -198,5 +204,7 @@ public class Opsu extends StateBasedGame {
ErrorHandler.error("Failed to close server socket.", e, false);
}
}
System.exit(0);
}
}

View File

@ -58,6 +58,9 @@ public class Options {
"Songs/"
};
/** Score database name. */
public static final String SCORE_DB = ".opsu_scores.db";
/** Font file name. */
public static final String FONT_NAME = "kochi-gothic.ttf";

View File

@ -18,6 +18,8 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@ -52,9 +54,10 @@ public class OsuGroupNode {
* Draws the button.
* @param x the x coordinate
* @param y the y coordinate
* @param grade the highest grade, if any
* @param focus true if this is the focused node
*/
public void draw(float x, float y, boolean focus) {
public void draw(float x, float y, Grade grade, boolean focus) {
boolean expanded = (osuFileIndex > -1);
float xOffset = 0f;
OsuFile osu;
@ -77,6 +80,13 @@ public class OsuGroupNode {
float cx = x + (bg.getWidth() * 0.05f) - xOffset;
float cy = y + (bg.getHeight() * 0.2f) - 3;
if (grade != Grade.NULL) {
Image gradeImg = grade.getSmallImage();
gradeImg = gradeImg.getScaledCopy((bg.getHeight() * 0.45f) / gradeImg.getHeight());
gradeImg.drawCentered(cx - bg.getWidth() * 0.01f + gradeImg.getWidth() / 2f, y + bg.getHeight() / 2.2f);
cx += gradeImg.getWidth();
}
Utils.FONT_MEDIUM.drawString(cx, cy, osu.getTitle(), textColor);
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 4,
String.format("%s // %s", osu.getArtist(), osu.creator), textColor);

View File

@ -0,0 +1,338 @@
/*
* 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;
import itdelatrisu.opsu.GameData.Grade;
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.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;
/**
* Handles game score data.
*/
public class Scores {
/** Class encapsulating all score data. */
public static class ScoreData implements Comparable<ScoreData> {
/** 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;
/**
* 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 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() {
return GameData.getGrade(hit300, hit100, hit50, miss);
}
@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<ScoreData> list = new ArrayList<ScoreData>();
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<String, ScoreData[]> getMapSetScores(OsuFile osu) {
Map<String, ScoreData[]> map = new HashMap<String, ScoreData[]>();
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<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 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);
}
}
}

View File

@ -28,6 +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.Utils;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
@ -160,7 +161,6 @@ public class Game extends BasicGameState {
// create the associated GameData object
data = new GameData(width, height);
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
}
@Override
@ -419,8 +419,12 @@ public class Game extends BasicGameState {
if (objectIndex >= osu.objects.length) {
if (checkpointLoaded) // if checkpoint used, skip ranking screen
game.closeRequested();
else // go to ranking screen
else { // go to ranking screen
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
if (!GameMod.AUTO.isActive())
Scores.addScore(data.getScoreData(osu));
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
return;
}
@ -800,7 +804,7 @@ public class Game extends BasicGameState {
// load other images...
((GamePauseMenu) game.getState(Opsu.STATE_GAMEPAUSEMENU)).loadImages();
data.loadImages(osu.getFile().getParentFile());
data.loadImages();
}
/**

View File

@ -18,9 +18,8 @@
package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.OsuFile;
@ -100,29 +99,13 @@ public class GameRanking extends BasicGameState {
g.setBackground(Utils.COLOR_BLACK_ALPHA);
// ranking screen elements
data.drawRankingElements(g, width, height);
// game mods
for (GameMod mod : GameMod.VALUES_REVERSED) {
if (mod.isActive()) {
Image modImage = mod.getImage();
modImage.draw(
(width * 0.75f) + ((mod.getID() - (GameMod.SIZE / 2)) * modImage.getWidth() / 3f),
height / 2f
);
}
}
// header text
float marginX = width * 0.01f, marginY = height * 0.01f;
Utils.FONT_LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", osu.getArtist(), osu.getTitle(), osu.version), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", osu.creator), Color.white);
data.drawRankingElements(g, osu);
// buttons
retryButton.draw();
exitButton.draw();
if (data.isGameplay()) {
retryButton.draw();
exitButton.draw();
}
Utils.getBackButton().draw();
Utils.drawVolume(g);
@ -146,12 +129,7 @@ public class GameRanking extends BasicGameState {
public void keyPressed(int key, char c) {
switch (key) {
case Input.KEY_ESCAPE:
SoundController.playSound(SoundEffect.MENUBACK);
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
songMenu.resetGameDataOnLoad();
songMenu.resetTrackOnLoad();
Utils.resetCursor();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
returnToSongMenu();
break;
case Input.KEY_F12:
Utils.takeScreenShot();
@ -165,25 +143,26 @@ public class GameRanking extends BasicGameState {
if (button != Input.MOUSE_LEFT_BUTTON)
return;
if (retryButton.contains(x, y)) {
OsuFile osu = MusicController.getOsuFile();
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.MANUAL);
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else if (exitButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUBACK);
((MainMenu) game.getState(Opsu.STATE_MAINMENU)).reset();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
Utils.resetCursor();
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else if (Utils.getBackButton().contains(x, y)) {
SoundController.playSound(SoundEffect.MENUBACK);
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
songMenu.resetGameDataOnLoad();
songMenu.resetTrackOnLoad();
Utils.resetCursor();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
if (data.isGameplay()) {
if (retryButton.contains(x, y)) {
OsuFile osu = MusicController.getOsuFile();
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.MANUAL);
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return;
} else if (exitButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUBACK);
((MainMenu) game.getState(Opsu.STATE_MAINMENU)).reset();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
Utils.resetCursor();
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return;
}
}
if (Utils.getBackButton().contains(x, y)) {
returnToSongMenu();
return;
}
}
@ -195,6 +174,26 @@ public class GameRanking extends BasicGameState {
SoundController.playSound(SoundEffect.APPLAUSE);
}
@Override
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
this.data = null;
}
/**
* Returns to the song menu.
*/
private void returnToSongMenu() {
SoundController.playSound(SoundEffect.MENUBACK);
if (data.isGameplay()) {
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
songMenu.resetGameDataOnLoad();
songMenu.resetTrackOnLoad();
}
Utils.resetCursor();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
/**
* Sets the associated GameData object.
* @param data the GameData

View File

@ -28,6 +28,9 @@ import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Scores;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.Scores.ScoreData;
import itdelatrisu.opsu.SongSort;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound;
@ -36,6 +39,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import java.io.File;
import java.util.Map;
import java.util.Stack;
import org.lwjgl.opengl.Display;
@ -153,6 +157,9 @@ public class SongMenu extends BasicGameState {
/** Beatmap reloading thread. */
private Thread reloadThread;
/** Current map of scores (Version, ScoreData[]). */
private Map<String, ScoreData[]> scoreMap;
// game-related variables
private GameContainer container;
private StateBasedGame game;
@ -261,8 +268,16 @@ public class SongMenu extends BasicGameState {
// song buttons
OsuGroupNode node = startNode;
for (int i = 0; i < MAX_BUTTONS && node != null; i++, node = node.next) {
// draw the node
float offset = (i == hoverIndex) ? hoverOffset : 0f;
node.draw(buttonX - offset, buttonY + (i*buttonOffset), (node == focusNode));
ScoreData[] scores = getScoreDataForNode(node);
node.draw(
buttonX - offset, buttonY + (i*buttonOffset),
(scores == null) ? Grade.NULL : scores[0].getGrade(),
(node == focusNode)
);
// load glyphs
Utils.loadGlyphs(node.osuFiles.get(0));
}
@ -703,11 +718,17 @@ public class SongMenu extends BasicGameState {
// reset game data
if (resetGame) {
((Game) game.getState(Opsu.STATE_GAME)).resetGameData();
// destroy skin images, if any
for (GameImage img : GameImage.values()) {
if (img.isSkinnable())
img.destroySkinImage();
}
// reload scores
if (focusNode != null)
scoreMap = Scores.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex));
resetGame = false;
}
}
@ -798,6 +819,9 @@ public class SongMenu extends BasicGameState {
MusicController.play(osu, true);
Utils.loadGlyphs(osu);
// load scores
scoreMap = Scores.getMapSetScores(osu);
// check startNode bounds
while (startNode.index >= OsuGroupList.get().size() + length - MAX_BUTTONS && startNode.prev != null)
startNode = startNode.prev;
@ -835,6 +859,30 @@ public class SongMenu extends BasicGameState {
*/
public void resetTrackOnLoad() { resetTrack = true; }
/**
* Returns all the score data for an OsuGroupNode from scoreMap.
* If no score data is available for the node, return null.
* @param node the OsuGroupNode
* @return the ScoreData array
*/
private ScoreData[] getScoreDataForNode(OsuGroupNode node) {
if (scoreMap == null || node.osuFileIndex == -1) // node not expanded
return null;
OsuFile osu = node.osuFiles.get(node.osuFileIndex);
ScoreData[] scores = scoreMap.get(osu.version);
if (scores == null || scores.length < 1) // no scores
return null;
ScoreData s = scores[0];
if (osu.beatmapID == s.MID && osu.beatmapSetID == s.MSID &&
osu.title.equals(s.title) && osu.artist.equals(s.artist) &&
osu.creator.equals(s.creator))
return scores;
else
return null; // incorrect map
}
/**
* Starts the game.
* @param osu the OsuFile to send to the game