Added replay saving/loading and score database auto-updater.

- Added 'info' table to score database to store the database version.  Upon startup, if the stored version is less than the source version, all update queries defined in ScoreDB.getUpdateQueries() will be run.
- Created "Replays" directory to store replay files.  Replay files are created after gameplay.
- Added 'replay' column to the score database to hold replay file names.
- Created a Game.loadOsuFile() method to load game data.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-03-11 20:52:51 -04:00
parent 37c0763f32
commit 712cf30e01
9 changed files with 180 additions and 35 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/Replays/
/Screenshots/ /Screenshots/
/Skins/ /Skins/
/SongPacks/ /SongPacks/

View File

@ -26,6 +26,7 @@ import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.replay.ReplayFrame;
import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -325,7 +326,8 @@ public class GameData {
hitResultCount[HIT_300K] = 0; hitResultCount[HIT_300K] = 0;
hitResultCount[HIT_100K] = s.katu; hitResultCount[HIT_100K] = s.katu;
hitResultCount[HIT_MISS] = s.miss; hitResultCount[HIT_MISS] = s.miss;
this.replay = s.replay; this.replay = (s.replayString == null) ? null :
new Replay(new File(Options.getReplayDir(), String.format("%s.osr", s.replayString)));
loadImages(); loadImages();
} }
@ -1197,10 +1199,9 @@ public class GameData {
* If score data already exists, the existing object will be returned * If score data already exists, the existing object will be returned
* (i.e. this will not overwrite existing data). * (i.e. this will not overwrite existing data).
* @param osu the OsuFile * @param osu the OsuFile
* @param frames the replay frames
* @return the ScoreData object * @return the ScoreData object
*/ */
public ScoreData getScoreData(OsuFile osu, ReplayFrame[] frames) { public ScoreData getScoreData(OsuFile osu) {
if (scoreData != null) if (scoreData != null)
return scoreData; return scoreData;
@ -1222,7 +1223,7 @@ public class GameData {
scoreData.combo = comboMax; scoreData.combo = comboMax;
scoreData.perfect = (comboMax == fullObjectCount); scoreData.perfect = (comboMax == fullObjectCount);
scoreData.mods = GameMod.getModState(); scoreData.mods = GameMod.getModState();
scoreData.replay = getReplay(frames); scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
return scoreData; return scoreData;
} }
@ -1245,7 +1246,7 @@ public class GameData {
replay.version = Updater.get().getBuildDate(); replay.version = Updater.get().getBuildDate();
replay.beatmapHash = ""; // TODO replay.beatmapHash = ""; // TODO
replay.playerName = ""; // TODO replay.playerName = ""; // TODO
replay.replayHash = ""; // TODO replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300]; replay.hit300 = (short) hitResultCount[HIT_300];
replay.hit100 = (short) hitResultCount[HIT_100]; replay.hit100 = (short) hitResultCount[HIT_100];
replay.hit50 = (short) hitResultCount[HIT_50]; replay.hit50 = (short) hitResultCount[HIT_50];
@ -1261,6 +1262,7 @@ public class GameData {
replay.frames = frames; replay.frames = frames;
replay.seed = 0; // TODO replay.seed = 0; // TODO
replay.loaded = true; replay.loaded = true;
return replay; return replay;
} }

View File

@ -90,6 +90,9 @@ public class Options {
/** The screenshot directory (created when needed). */ /** The screenshot directory (created when needed). */
private static File screenshotDir; private static File screenshotDir;
/** The replay directory (created when needed). */
private static File replayDir;
/** The current skin directory (for user skins). */ /** The current skin directory (for user skins). */
private static File skinDir; private static File skinDir;
@ -819,6 +822,19 @@ public class Options {
return screenshotDir; return screenshotDir;
} }
/**
* Returns the replay directory.
* If invalid, this will return a "Replay" directory.
* @return the replay directory
*/
public static File getReplayDir() {
if (replayDir != null && replayDir.isDirectory())
return replayDir;
replayDir = new File(DATA_DIR, "Replays/");
return replayDir;
}
/** /**
* Returns the current skin directory. * Returns the current skin directory.
* If invalid, this will create a "Skins" folder in the root directory. * If invalid, this will create a "Skins" folder in the root directory.
@ -892,6 +908,9 @@ public class Options {
case "ScreenshotDirectory": case "ScreenshotDirectory":
screenshotDir = new File(value); screenshotDir = new File(value);
break; break;
case "ReplayDirectory":
replayDir = new File(value);
break;
case "Skin": case "Skin":
skinDir = new File(value); skinDir = new File(value);
break; break;
@ -1054,6 +1073,8 @@ public class Options {
writer.newLine(); writer.newLine();
writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath())); writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath()));
writer.newLine(); writer.newLine();
writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath()));
writer.newLine();
writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath())); writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath()));
writer.newLine(); writer.newLine();
writer.write(String.format("ThemeSong = %s", themeString)); writer.write(String.format("ThemeSong = %s", themeString));

View File

@ -19,7 +19,6 @@
package itdelatrisu.opsu; package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade; import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.states.SongMenu; import itdelatrisu.opsu.states.SongMenu;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -60,8 +59,8 @@ public class ScoreData implements Comparable<ScoreData> {
/** Game mod bitmask. */ /** Game mod bitmask. */
public int mods; public int mods;
/** The replay. */ /** The replay string. */
public Replay replay; public String replayString;
/** Time since the score was achieved. */ /** Time since the score was achieved. */
private String timeSince; private String timeSince;
@ -157,7 +156,7 @@ public class ScoreData implements Comparable<ScoreData> {
this.combo = rs.getInt(15); this.combo = rs.getInt(15);
this.perfect = rs.getBoolean(16); this.perfect = rs.getBoolean(16);
this.mods = rs.getInt(17); this.mods = rs.getInt(17);
// this.replay = ; // TODO this.replayString = rs.getString(18);
} }
/** /**

View File

@ -32,6 +32,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -39,6 +40,29 @@ import java.util.Map;
* Handles connections and queries with the scores database. * Handles connections and queries with the scores database.
*/ */
public class ScoreDB { public class ScoreDB {
/**
* Current database version.
* This value should be changed whenever the database format changes.
* Add any update queries to the {@link #getUpdateQueries(int)} method.
*/
private static final int DATABASE_VERSION = 20140311;
/**
* Returns a list of SQL queries to apply, in order, to update from
* the given database version to the latest version.
* @param version the current version
* @return a list of SQL queries
*/
private static List<String> getUpdateQueries(int version) {
List<String> list = new LinkedList<String>();
if (version < 20140311)
list.add("ALTER TABLE scores ADD COLUMN replay TEXT");
/* add future updates here */
return list;
}
/** Database connection. */ /** Database connection. */
private static Connection connection; private static Connection connection;
@ -63,13 +87,16 @@ public class ScoreDB {
if (connection == null) if (connection == null)
return; return;
// run any database updates
updateDatabase();
// create the database // create the database
createDatabase(); createDatabase();
// prepare sql statements // prepare sql statements
try { try {
insertStmt = connection.prepareStatement( insertStmt = connection.prepareStatement(
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
); );
selectMapStmt = connection.prepareStatement( selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " + "SELECT * FROM scores WHERE " +
@ -109,15 +136,75 @@ public class ScoreDB {
"score INTEGER, " + "score INTEGER, " +
"combo INTEGER, " + "combo INTEGER, " +
"perfect BOOLEAN, " + "perfect BOOLEAN, " +
"mods INTEGER" + "mods INTEGER, " +
"replay TEXT" +
");" + ");" +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
"); " +
"CREATE INDEX IF NOT EXISTS idx ON scores (MID, MSID, title, artist, creator, version);"; "CREATE INDEX IF NOT EXISTS idx ON scores (MID, MSID, title, artist, creator, version);";
stmt.executeUpdate(sql); stmt.executeUpdate(sql);
// set the version key, if empty
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', %d)", DATABASE_VERSION);
stmt.executeUpdate(sql);
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Could not create score database.", e, true); ErrorHandler.error("Could not create score database.", e, true);
} }
} }
/**
* Applies any database updates by comparing the current version to the
* stored version. Does nothing if tables have not been created.
*/
private static void updateDatabase() {
try (Statement stmt = connection.createStatement()) {
int version = 0;
// if 'info' table does not exist, assume version 0 and apply all updates
String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
ResultSet rs = stmt.executeQuery(sql);
boolean infoExists = rs.isBeforeFirst();
rs.close();
if (!infoExists) {
// if 'scores' table also does not exist, databases not yet created
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'scores'";
ResultSet scoresRS = stmt.executeQuery(sql);
boolean scoresExists = scoresRS.isBeforeFirst();
scoresRS.close();
if (!scoresExists)
return;
} else {
// try to retrieve stored version
sql = "SELECT value FROM info WHERE key = 'version'";
ResultSet versionRS = stmt.executeQuery(sql);
String versionString = (versionRS.next()) ? versionRS.getString(1) : "0";
versionRS.close();
try {
version = Integer.parseInt(versionString);
} catch (NumberFormatException e) {}
}
// database versions match
if (version >= DATABASE_VERSION)
return;
// apply updates
for (String query : getUpdateQueries(version))
stmt.executeUpdate(query);
// update version
if (infoExists) {
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
ps.setString(1, Integer.toString(DATABASE_VERSION));
ps.executeUpdate();
ps.close();
}
} catch (SQLException e) {
ErrorHandler.error("Failed to update score database.", e, true);
}
}
/** /**
* Adds the game score to the database. * Adds the game score to the database.
* @param data the GameData object * @param data the GameData object
@ -128,6 +215,7 @@ public class ScoreDB {
try { try {
setStatementFields(insertStmt, data); setStatementFields(insertStmt, data);
insertStmt.setString(18, data.replayString);
insertStmt.executeUpdate(); insertStmt.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Failed to save score to database.", e, true); ErrorHandler.error("Failed to save score to database.", e, true);

View File

@ -19,6 +19,7 @@
package itdelatrisu.opsu.replay; package itdelatrisu.opsu.replay;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuReader; import itdelatrisu.opsu.OsuReader;
import itdelatrisu.opsu.OsuWriter; import itdelatrisu.opsu.OsuWriter;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
@ -217,10 +218,20 @@ public class Replay {
} }
/** /**
* Saves the replay data to a file. * Saves the replay data to a file in the replays directory.
* @param file the file to write to
*/ */
public void save(File file) { public void save() {
// create replay directory
File dir = Options.getReplayDir();
if (!dir.isDirectory()) {
if (!dir.mkdir()) {
ErrorHandler.error("Failed to create replay directory.", null, false);
return;
}
}
// write file
File file = new File(dir, String.format("%s.osr", getReplayFilename()));
try (FileOutputStream out = new FileOutputStream(file)) { try (FileOutputStream out = new FileOutputStream(file)) {
OsuWriter writer = new OsuWriter(out); OsuWriter writer = new OsuWriter(out);
@ -299,6 +310,18 @@ public class Replay {
} }
} }
/**
* Returns the file name of where the replay should be saved and loaded,
* or null if the required fields are not set.
*/
public String getReplayFilename() {
if (replayHash == null)
return null;
return String.format("%s-%d%d%d%d%d%d",
replayHash, hit300, hit100, hit50, geki, katu, miss);
}
@Override @Override
public String toString() { public String toString() {
final int LINE_SPLIT = 5; final int LINE_SPLIT = 5;

View File

@ -27,6 +27,7 @@ import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OsuTimingPoint; import itdelatrisu.opsu.OsuTimingPoint;
import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.UI; import itdelatrisu.opsu.UI;
@ -35,6 +36,7 @@ import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.db.OsuDB;
import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.Circle;
import itdelatrisu.opsu.objects.HitObject; import itdelatrisu.opsu.objects.HitObject;
@ -49,6 +51,7 @@ import java.util.Stack;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
import org.newdawn.slick.Animation; import org.newdawn.slick.Animation;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer; import org.newdawn.slick.GameContainer;
@ -483,16 +486,17 @@ public class Game extends BasicGameState {
// go to ranking screen // go to ranking screen
else { else {
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
ReplayFrame[] rf = null;
if (!isReplay && replayFrames != null) { if (!isReplay && replayFrames != null) {
// finalize replay frames with start/skip frames // finalize replay frames with start/skip frames
if (!replayFrames.isEmpty()) if (!replayFrames.isEmpty())
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1); replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0)); replayFrames.addFirst(ReplayFrame.getStartFrame(0));
rf = replayFrames.toArray(new ReplayFrame[replayFrames.size()]); Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]));
if (r != null)
r.save();
} }
ScoreData score = data.getScoreData(osu, rf); ScoreData score = data.getScoreData(osu);
// add score to database // add score to database
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay) if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
@ -830,8 +834,6 @@ public class Game extends BasicGameState {
public void enter(GameContainer container, StateBasedGame game) public void enter(GameContainer container, StateBasedGame game)
throws SlickException { throws SlickException {
UI.enter(); UI.enter();
if (restart == Restart.NEW)
osu = MusicController.getOsuFile();
if (osu == null || osu.objects == null) if (osu == null || osu.objects == null)
throw new RuntimeException("Running game with no OsuFile loaded."); throw new RuntimeException("Running game with no OsuFile loaded.");
@ -978,6 +980,19 @@ public class Game extends BasicGameState {
} }
} }
/**
* Loads all required data from an OsuFile.
* @param osu the OsuFile to load
*/
public void loadOsuFile(OsuFile osu) {
this.osu = osu;
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
if (osu.timingPoints == null || osu.combo == null)
OsuDB.load(osu, OsuDB.LOAD_ARRAY);
OsuParser.parseHitObjects(osu);
HitSound.setDefaultSampleSet(osu.sampleSet);
}
/** /**
* Resets all game data and structures. * Resets all game data and structures.
*/ */

View File

@ -163,27 +163,29 @@ public class GameRanking extends BasicGameState {
} }
// replay // replay
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
boolean returnToGame = false; boolean returnToGame = false;
if (replayButton.contains(x, y)) { if (replayButton.contains(x, y)) {
Replay r = data.getReplay(null); Replay r = data.getReplay(null);
if (r != null) { if (r != null) {
r.load(); r.load();
((Game) game.getState(Opsu.STATE_GAME)).setReplay(r); gameState.setReplay(r);
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.REPLAY); gameState.setRestart((data.isGameplay()) ? Game.Restart.REPLAY : Game.Restart.NEW);
returnToGame = true; returnToGame = true;
} } else
UI.sendBarNotification("Replay file not found.");
} }
// retry // retry
else if (data.isGameplay() && retryButton.contains(x, y)) { else if (data.isGameplay() && retryButton.contains(x, y)) {
((Game) game.getState(Opsu.STATE_GAME)).setReplay(null); gameState.setReplay(null);
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.MANUAL); gameState.setRestart(Game.Restart.MANUAL);
returnToGame = true; returnToGame = true;
} }
if (returnToGame) { if (returnToGame) {
OsuFile osu = MusicController.getOsuFile(); OsuFile osu = MusicController.getOsuFile();
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString())); gameState.loadOsuFile(osu);
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return; return;
@ -211,6 +213,8 @@ public class GameRanking extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game) public void leave(GameContainer container, StateBasedGame game)
throws SlickException { throws SlickException {
this.data = null; this.data = null;
if (MusicController.isTrackDimmed())
MusicController.toggleTrackDimmed(1f);
} }
/** /**

View File

@ -34,7 +34,6 @@ import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.SongSort; import itdelatrisu.opsu.SongSort;
import itdelatrisu.opsu.UI; import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MultiClip; import itdelatrisu.opsu.audio.MultiClip;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
@ -1288,17 +1287,10 @@ public class SongMenu extends BasicGameState {
return; return;
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
OsuFile osu = MusicController.getOsuFile();
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
// load any missing data
if (osu.timingPoints == null || osu.combo == null)
OsuDB.load(osu, OsuDB.LOAD_ARRAY);
OsuParser.parseHitObjects(osu);
HitSound.setDefaultSampleSet(osu.sampleSet);
MultiClip.destroyExtraClips(); MultiClip.destroyExtraClips();
OsuFile osu = MusicController.getOsuFile();
Game gameState = (Game) game.getState(Opsu.STATE_GAME); Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadOsuFile(osu);
gameState.setRestart(Game.Restart.NEW); gameState.setRestart(Game.Restart.NEW);
gameState.setReplay(null); gameState.setReplay(null);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));