diff --git a/.gitignore b/.gitignore index 1668a52a..172616ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/Replays/ /Screenshots/ /Skins/ /SongPacks/ diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 2d9ba80e..4bb14094 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; +import java.io.File; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -325,7 +326,8 @@ public class GameData { hitResultCount[HIT_300K] = 0; hitResultCount[HIT_100K] = s.katu; 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(); } @@ -1197,10 +1199,9 @@ public class GameData { * If score data already exists, the existing object will be returned * (i.e. this will not overwrite existing data). * @param osu the OsuFile - * @param frames the replay frames * @return the ScoreData object */ - public ScoreData getScoreData(OsuFile osu, ReplayFrame[] frames) { + public ScoreData getScoreData(OsuFile osu) { if (scoreData != null) return scoreData; @@ -1222,7 +1223,7 @@ public class GameData { scoreData.combo = comboMax; scoreData.perfect = (comboMax == fullObjectCount); scoreData.mods = GameMod.getModState(); - scoreData.replay = getReplay(frames); + scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); return scoreData; } @@ -1245,7 +1246,7 @@ public class GameData { replay.version = Updater.get().getBuildDate(); replay.beatmapHash = ""; // TODO replay.playerName = ""; // TODO - replay.replayHash = ""; // TODO + replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO replay.hit300 = (short) hitResultCount[HIT_300]; replay.hit100 = (short) hitResultCount[HIT_100]; replay.hit50 = (short) hitResultCount[HIT_50]; @@ -1261,6 +1262,7 @@ public class GameData { replay.frames = frames; replay.seed = 0; // TODO replay.loaded = true; + return replay; } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index f43e1365..b4ef7add 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -90,6 +90,9 @@ public class Options { /** The screenshot directory (created when needed). */ private static File screenshotDir; + /** The replay directory (created when needed). */ + private static File replayDir; + /** The current skin directory (for user skins). */ private static File skinDir; @@ -819,6 +822,19 @@ public class Options { 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. * If invalid, this will create a "Skins" folder in the root directory. @@ -892,6 +908,9 @@ public class Options { case "ScreenshotDirectory": screenshotDir = new File(value); break; + case "ReplayDirectory": + replayDir = new File(value); + break; case "Skin": skinDir = new File(value); break; @@ -1054,6 +1073,8 @@ public class Options { writer.newLine(); writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath())); writer.newLine(); + writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath())); + writer.newLine(); writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath())); writer.newLine(); writer.write(String.format("ThemeSong = %s", themeString)); diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 9152429e..970fa3fe 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -19,7 +19,6 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.GameData.Grade; -import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.states.SongMenu; import java.sql.ResultSet; @@ -60,8 +59,8 @@ public class ScoreData implements Comparable { /** Game mod bitmask. */ public int mods; - /** The replay. */ - public Replay replay; + /** The replay string. */ + public String replayString; /** Time since the score was achieved. */ private String timeSince; @@ -157,7 +156,7 @@ public class ScoreData implements Comparable { this.combo = rs.getInt(15); this.perfect = rs.getBoolean(16); this.mods = rs.getInt(17); -// this.replay = ; // TODO + this.replayString = rs.getString(18); } /** diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java index 25976bb9..7744992d 100644 --- a/src/itdelatrisu/opsu/db/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -39,6 +40,29 @@ import java.util.Map; * Handles connections and queries with the scores database. */ public class ScoreDB { + /** + * Current database version. + * This value should be changed whenever the database format changes. + * Add any update queries to the {@link #getUpdateQueries(int)} method. + */ + private static final int DATABASE_VERSION = 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 getUpdateQueries(int version) { + List list = new LinkedList(); + if (version < 20140311) + list.add("ALTER TABLE scores ADD COLUMN replay TEXT"); + + /* add future updates here */ + + return list; + } + /** Database connection. */ private static Connection connection; @@ -63,13 +87,16 @@ public class ScoreDB { if (connection == null) return; + // run any database updates + updateDatabase(); + // create the database createDatabase(); // prepare sql statements try { insertStmt = connection.prepareStatement( - "INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + "INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ); selectMapStmt = connection.prepareStatement( "SELECT * FROM scores WHERE " + @@ -109,15 +136,75 @@ public class ScoreDB { "score INTEGER, " + "combo INTEGER, " + "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);"; stmt.executeUpdate(sql); + + // set the version key, if empty + sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', %d)", DATABASE_VERSION); + stmt.executeUpdate(sql); } catch (SQLException e) { ErrorHandler.error("Could not create score database.", e, true); } } + /** + * Applies any database updates by comparing the current version to the + * stored version. Does nothing if tables have not been created. + */ + private static void updateDatabase() { + try (Statement stmt = connection.createStatement()) { + int version = 0; + + // if 'info' table does not exist, assume version 0 and apply all updates + String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'"; + ResultSet rs = stmt.executeQuery(sql); + boolean infoExists = rs.isBeforeFirst(); + rs.close(); + if (!infoExists) { + // if 'scores' table also does not exist, databases not yet created + sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'scores'"; + ResultSet scoresRS = stmt.executeQuery(sql); + boolean scoresExists = scoresRS.isBeforeFirst(); + scoresRS.close(); + if (!scoresExists) + return; + } else { + // try to retrieve stored version + sql = "SELECT value FROM info WHERE key = 'version'"; + ResultSet versionRS = stmt.executeQuery(sql); + String versionString = (versionRS.next()) ? versionRS.getString(1) : "0"; + versionRS.close(); + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException e) {} + } + + // database versions match + if (version >= DATABASE_VERSION) + return; + + // apply updates + for (String query : getUpdateQueries(version)) + stmt.executeUpdate(query); + + // update version + if (infoExists) { + PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)"); + ps.setString(1, Integer.toString(DATABASE_VERSION)); + ps.executeUpdate(); + ps.close(); + } + } catch (SQLException e) { + ErrorHandler.error("Failed to update score database.", e, true); + } + } + /** * Adds the game score to the database. * @param data the GameData object @@ -128,6 +215,7 @@ public class ScoreDB { try { setStatementFields(insertStmt, data); + insertStmt.setString(18, data.replayString); insertStmt.executeUpdate(); } catch (SQLException e) { ErrorHandler.error("Failed to save score to database.", e, true); diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java index 73d41a73..98273025 100644 --- a/src/itdelatrisu/opsu/replay/Replay.java +++ b/src/itdelatrisu/opsu/replay/Replay.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu.replay; import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Options; import itdelatrisu.opsu.OsuReader; import itdelatrisu.opsu.OsuWriter; import itdelatrisu.opsu.Utils; @@ -217,10 +218,20 @@ public class Replay { } /** - * Saves the replay data to a file. - * @param file the file to write to + * Saves the replay data to a file in the replays directory. */ - 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)) { 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 public String toString() { final int LINE_SPLIT = 5; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index a78bf298..613cf610 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -27,6 +27,7 @@ import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuHitObject; +import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.OsuTimingPoint; import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.UI; @@ -35,6 +36,7 @@ import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.db.OsuDB; import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.HitObject; @@ -49,6 +51,7 @@ import java.util.Stack; import java.util.concurrent.TimeUnit; import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.Display; import org.newdawn.slick.Animation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -483,16 +486,17 @@ public class Game extends BasicGameState { // go to ranking screen else { ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); - ReplayFrame[] rf = null; if (!isReplay && replayFrames != null) { // finalize replay frames with start/skip frames if (!replayFrames.isEmpty()) replayFrames.getFirst().setTimeDiff(replaySkipTime * -1); replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); 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 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) throws SlickException { UI.enter(); - if (restart == Restart.NEW) - osu = MusicController.getOsuFile(); if (osu == null || osu.objects == null) 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. */ diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index f7842b76..27bc53b4 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -163,27 +163,29 @@ public class GameRanking extends BasicGameState { } // replay + Game gameState = (Game) game.getState(Opsu.STATE_GAME); boolean returnToGame = false; if (replayButton.contains(x, y)) { Replay r = data.getReplay(null); if (r != null) { r.load(); - ((Game) game.getState(Opsu.STATE_GAME)).setReplay(r); - ((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.REPLAY); + gameState.setReplay(r); + gameState.setRestart((data.isGameplay()) ? Game.Restart.REPLAY : Game.Restart.NEW); returnToGame = true; - } + } else + UI.sendBarNotification("Replay file not found."); } // retry else if (data.isGameplay() && retryButton.contains(x, y)) { - ((Game) game.getState(Opsu.STATE_GAME)).setReplay(null); - ((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.MANUAL); + gameState.setReplay(null); + gameState.setRestart(Game.Restart.MANUAL); returnToGame = true; } if (returnToGame) { OsuFile osu = MusicController.getOsuFile(); - Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString())); + gameState.loadOsuFile(osu); SoundController.playSound(SoundEffect.MENUHIT); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); return; @@ -211,6 +213,8 @@ public class GameRanking extends BasicGameState { public void leave(GameContainer container, StateBasedGame game) throws SlickException { this.data = null; + if (MusicController.isTrackDimmed()) + MusicController.toggleTrackDimmed(1f); } /** diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index bc789141..cdba346d 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -34,7 +34,6 @@ import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.SongSort; import itdelatrisu.opsu.UI; import itdelatrisu.opsu.Utils; -import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.MultiClip; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; @@ -1288,17 +1287,10 @@ public class SongMenu extends BasicGameState { return; 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(); + OsuFile osu = MusicController.getOsuFile(); Game gameState = (Game) game.getState(Opsu.STATE_GAME); + gameState.loadOsuFile(osu); gameState.setRestart(Game.Restart.NEW); gameState.setReplay(null); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));