From d860a30aed3dcca31ac15e700e09bb93f2db51a1 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 29 Jun 2015 19:22:38 -0500 Subject: [PATCH] Follow-up to #99. - Many code style changes. - Don't increment combo if missing the last slider circle. - Added player name in ranking screen. - Don't show null/default player names. - Only import replays with .osr extension. - Display loading status for importing replays. - Moved MD5InputStreamWrapper to package "opsu.io". Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ErrorHandler.java | 2 +- src/itdelatrisu/opsu/GameData.java | 78 +++++----- .../opsu/MD5InputStreamWrapper.java | 91 ------------ src/itdelatrisu/opsu/Options.java | 4 +- src/itdelatrisu/opsu/OszUnpacker.java | 2 +- src/itdelatrisu/opsu/ScoreData.java | 14 +- src/itdelatrisu/opsu/beatmap/Beatmap.java | 6 +- .../opsu/beatmap/BeatmapParser.java | 25 ++-- .../opsu/beatmap/BeatmapSetList.java | 26 ++-- src/itdelatrisu/opsu/db/BeatmapDB.java | 4 +- src/itdelatrisu/opsu/db/ScoreDB.java | 10 +- .../opsu/io/MD5InputStreamWrapper.java | 118 +++++++++++++++ src/itdelatrisu/opsu/objects/Circle.java | 25 ++-- src/itdelatrisu/opsu/objects/DummyObject.java | 4 +- src/itdelatrisu/opsu/objects/GameObject.java | 6 +- src/itdelatrisu/opsu/objects/Slider.java | 80 +++++------ src/itdelatrisu/opsu/objects/Spinner.java | 109 +++++++------- .../opsu/render/CurveRenderState.java | 18 +-- .../opsu/render/FrameBufferCache.java | 12 +- src/itdelatrisu/opsu/render/Rendertarget.java | 7 +- src/itdelatrisu/opsu/replay/Replay.java | 79 ++++++----- .../opsu/replay/ReplayImporter.java | 134 +++++++++++++----- .../opsu/states/DownloadsMenu.java | 2 +- src/itdelatrisu/opsu/states/Game.java | 48 +++---- src/itdelatrisu/opsu/states/MainMenu.java | 2 +- src/itdelatrisu/opsu/states/SongMenu.java | 2 +- src/itdelatrisu/opsu/states/Splash.java | 6 +- src/itdelatrisu/opsu/ui/UI.java | 4 + .../slick/openal/OpenALStreamPlayer.java | 21 +-- 29 files changed, 513 insertions(+), 426 deletions(-) delete mode 100644 src/itdelatrisu/opsu/MD5InputStreamWrapper.java create mode 100644 src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java index 3976902e..31798388 100644 --- a/src/itdelatrisu/opsu/ErrorHandler.java +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -74,7 +74,7 @@ public class ErrorHandler { /** * Displays an error popup and logs the given error. - * @param error a descR of the error + * @param error a description of the error * @param e the exception causing the error * @param report whether to ask to report the error */ diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 7e14b995..b3d29e12 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -838,8 +838,9 @@ public class GameData { String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white); Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6, String.format("Beatmap by %s", beatmap.creator), Color.white); + String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName); Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10, - String.format("Played on %s.", scoreData.getTimeString()), Color.white); + String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white); // mod icons int modWidth = GameMod.AUTO.getImage().getWidth(); @@ -1207,13 +1208,8 @@ public class GameData { } fullObjectCount++; } - public void sliderFinalResult(int time, int hitSlider30, float x, float y, - HitObject hitObject, int currentRepeats) { - score += 30; - } /** - * https://osu.ppy.sh/wiki/Score * Returns the score for a hit based on the following score formula: *

* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25 @@ -1224,26 +1220,31 @@ public class GameData { *

  • Mod: mod multipliers * * @param hitValue the hit value - * @param hitObject + * @param hitObject the hit object * @return the score value + * @see https://osu.ppy.sh/wiki/Score */ private int getScoreForHit(int hitValue, HitObject hitObject) { - int comboMulti = Math.max(combo - 1, 0); - if(hitObject.isSlider()){ - comboMulti += 1; - } - return (hitValue + (int)(hitValue * (comboMulti * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25)); + int comboMultiplier = Math.max(combo - 1, 0); + if (hitObject.isSlider()) + comboMultiplier++; + return (hitValue + (int)(hitValue * (comboMultiplier * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25)); } + /** - * https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier * Computes and stores the difficulty multiplier used in the score formula. * @param drainRate the raw HP drain rate value * @param circleSize the raw circle size value * @param overallDifficulty the raw overall difficulty value + * @see https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier */ public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) { - //TODO THE LIES ( difficultyMultiplier ) - //* + // TODO: find the actual formula (osu!wiki is wrong) + // seems to be based on hit object density? (total objects / time) + // 924 3x1/4 beat notes 0.14stars + // 924 3x1beat 0.28stars + // 912 3x1beat with 1 extra note 10 sec away 0.29stars + float sum = drainRate + circleSize + overallDifficulty; // typically 2~27 if (sum <= 5f) difficultyMultiplier = 2; @@ -1255,21 +1256,11 @@ public class GameData { difficultyMultiplier = 5; else //if (sum <= 30f) difficultyMultiplier = 6; - //*/ - - /* - 924 3x1/4 beat notes 0.14stars - 924 3x1beat 0.28stars - 912 3x1beat wth 1 extra note 10 sec away 0.29stars - - seems to be based on hitobject density? (Total Objects/Time) - */ - /* - float mult = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f; - System.out.println("diffuculty Multiplier : "+ mult); - difficultyMultiplier = (int)mult; - */ + + //float multiplier = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f; + //difficultyMultiplier = (int) multiplier; } + /** * Handles a hit result and performs all associated calculations. * @param time the object start time @@ -1279,12 +1270,13 @@ public class GameData { * @param color the combo color * @param end true if this is the last hit object in the combo * @param hitObject the hit object - * @param repeat the current repeat number (for sliders, or 0 otherwise) * @param hitResultType the type of hit object for the result + * @param repeat the current repeat number (for sliders, or 0 otherwise) + * @param noIncrementCombo if the combo should not be incremented by this result * @return the actual hit result (HIT_* constants) */ - private int handleHitResult(int time, int result, float x, float y, Color color, - boolean end, HitObject hitObject, int repeat, HitObjectType hitResultType) { + private int handleHitResult(int time, int result, float x, float y, Color color, boolean end, + HitObject hitObject, HitObjectType hitResultType, int repeat, boolean noIncrementCombo) { // update health, score, and combo streak based on hit result int hitValue = 0; switch (result) { @@ -1318,7 +1310,8 @@ public class GameData { // calculate score and increment combo streak changeScore(getScoreForHit(hitValue, hitObject)); - incrementComboStreak(); + if (!noIncrementCombo) + incrementComboStreak(); } hitResultCount[result]++; fullObjectCount++; @@ -1347,7 +1340,7 @@ public class GameData { } /** - * Handles a slider hit result. + * Handles a hit result. * @param time the object start time * @param result the hit result (HIT_* constants) * @param x the x coordinate @@ -1355,15 +1348,17 @@ public class GameData { * @param color the combo color * @param end true if this is the last hit object in the combo * @param hitObject the hit object - * @param repeat the current repeat number (for sliders, or 0 otherwise) * @param hitResultType the type of hit object for the result - * @param curve the slider curve (or null if not applicable) * @param expand whether or not the hit result animation should expand (if applicable) + * @param repeat the current repeat number (for sliders, or 0 otherwise) + * @param curve the slider curve (or null if not applicable) + * @param sliderHeldToEnd whether or not the slider was held to the end (if applicable) */ public void hitResult(int time, int result, float x, float y, Color color, - boolean end, HitObject hitObject, int repeat, - HitObjectType hitResultType, Curve curve, boolean expand) { - int hitResult = handleHitResult(time, result, x, y, color, end, hitObject, repeat, hitResultType); + boolean end, HitObject hitObject, HitObjectType hitResultType, + boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) { + int hitResult = handleHitResult(time, result, x, y, color, end, hitObject, + hitResultType, repeat, (curve != null && !sliderHeldToEnd)); if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results @@ -1382,7 +1377,6 @@ public class GameData { } } - /** * Returns a ScoreData object encapsulating all game data. * If score data already exists, the existing object will be returned @@ -1413,7 +1407,7 @@ public class GameData { scoreData.perfect = (comboMax == fullObjectCount); scoreData.mods = GameMod.getModState(); scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); - scoreData.playerName = "OpsuPlayer"; //TODO GameDataPlayerName? + scoreData.playerName = null; // TODO return scoreData; } @@ -1434,7 +1428,7 @@ public class GameData { replay = new Replay(); replay.mode = Beatmap.MODE_OSU; replay.version = Updater.get().getBuildDate(); - replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash;//Utils.getMD5(beatmap.getFile()); + replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash; replay.playerName = ""; // TODO replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO replay.hit300 = (short) hitResultCount[HIT_300]; diff --git a/src/itdelatrisu/opsu/MD5InputStreamWrapper.java b/src/itdelatrisu/opsu/MD5InputStreamWrapper.java deleted file mode 100644 index 530d50f7..00000000 --- a/src/itdelatrisu/opsu/MD5InputStreamWrapper.java +++ /dev/null @@ -1,91 +0,0 @@ -package itdelatrisu.opsu; - -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class MD5InputStreamWrapper extends InputStream { - - InputStream in; - private boolean eof; // End Of File - MessageDigest md; - public MD5InputStreamWrapper(InputStream in) throws NoSuchAlgorithmException { - this.in = in; - md = MessageDigest.getInstance("MD5"); - } - - @Override - public int read() throws IOException { - int readed = in.read(); - if(readed>=0) - md.update((byte) readed); - else - eof=true; - return readed; - } - - @Override - public int available() throws IOException { - return in.available(); - } - - @Override - public void close() throws IOException { - in.close(); - } - - @Override - public synchronized void mark(int readlimit) { - in.mark(readlimit); - } - - @Override - public boolean markSupported() { - return in.markSupported(); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int readed = in.read(b, off, len); - if(readed>=0) - md.update(b, off, readed); - else - eof=true; - - return readed; - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0 ,b.length); - } - - @Override - public synchronized void reset() throws IOException { - throw new RuntimeException("MD5 stream not resetable"); - } - - @Override - public long skip(long n) throws IOException { - throw new RuntimeException("MD5 stream not skipable"); - } - - public String getMD5() throws IOException { - byte[] buf = null; - if(!eof) - buf = new byte[0x1000]; - while(!eof){ - read(buf); - } - - byte[] md5byte = md.digest(); - StringBuilder result = new StringBuilder(); - for (byte b : md5byte) - result.append(String.format("%02x", b)); - //System.out.println("MD5 stream md5 " + result.toString()); - return result.toString(); - - } - -} diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 9ff3e388..b5072e0e 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -1090,7 +1090,7 @@ public class Options { oszDir.mkdir(); return oszDir; } - + /** * Returns the replay import directory. * If invalid, this will create and return a "ReplayImport" directory. @@ -1104,7 +1104,7 @@ public class Options { replayImportDir.mkdir(); return replayImportDir; } - + /** * Returns the screenshot directory. * If invalid, this will return a "Screenshot" directory. diff --git a/src/itdelatrisu/opsu/OszUnpacker.java b/src/itdelatrisu/opsu/OszUnpacker.java index b572de50..b8bf1460 100644 --- a/src/itdelatrisu/opsu/OszUnpacker.java +++ b/src/itdelatrisu/opsu/OszUnpacker.java @@ -33,7 +33,7 @@ public class OszUnpacker { /** The index of the current file being unpacked. */ private static int fileIndex = -1; - /** The total number of directories to parse. */ + /** The total number of files to unpack. */ private static File[] files; // This class should not be instantiated. diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index b6307031..881a6fe2 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -63,6 +63,9 @@ public class ScoreData implements Comparable { /** The replay string. */ public String replayString; + /** The player name. */ + public String playerName; + /** Time since the score was achieved. */ private String timeSince; @@ -77,9 +80,6 @@ public class ScoreData implements Comparable { /** The tooltip string. */ private String tooltip; - - /** The players Name. */ - public String playerName; /** Drawing values. */ private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset; @@ -262,9 +262,10 @@ public class ScoreData implements Comparable { ); // hit counts (custom: osu! shows user instead, above score) + String player = (playerName == null) ? "" : String.format(" (%s)", playerName); Utils.FONT_SMALL.drawString( textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(), - String.format("300:%d 100:%d 50:%d Miss:%d Name:%s", hit300, hit100, hit50, miss, getPlayerName()), + String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player), Color.white ); @@ -335,11 +336,6 @@ public class ScoreData implements Comparable { ); } - public String getPlayerName() { - if(playerName == null) - return "Null Name"; - return playerName; - } @Override public int compareTo(ScoreData that) { if (this.score != that.score) diff --git a/src/itdelatrisu/opsu/beatmap/Beatmap.java b/src/itdelatrisu/opsu/beatmap/Beatmap.java index 7216b0de..8e102e15 100644 --- a/src/itdelatrisu/opsu/beatmap/Beatmap.java +++ b/src/itdelatrisu/opsu/beatmap/Beatmap.java @@ -184,10 +184,10 @@ public class Beatmap implements Comparable { /** Slider border color. If null, the skin value is used. */ public Color sliderBorder; - - /** md5 hash of this file */ + + /** MD5 hash of this file. */ public String md5Hash; - + /** * [HitObjects] */ diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index 315d8f7a..ef403b03 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -19,9 +19,9 @@ package itdelatrisu.opsu.beatmap; import itdelatrisu.opsu.ErrorHandler; -import itdelatrisu.opsu.MD5InputStreamWrapper; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.db.BeatmapDB; +import itdelatrisu.opsu.io.MD5InputStreamWrapper; import java.io.BufferedInputStream; import java.io.BufferedReader; @@ -68,6 +68,9 @@ public class BeatmapParser { /** The current status. */ private static Status status = Status.NONE; + /** If no Provider supports a MessageDigestSpi implementation for the MD5 algorithm. */ + private static boolean hasNoMD5Algorithm = false; + // This class should not be instantiated. private BeatmapParser() {} @@ -209,15 +212,11 @@ public class BeatmapParser { Beatmap beatmap = new Beatmap(file); beatmap.timingPoints = new ArrayList(); - try (InputStream inFileStream = new BufferedInputStream(new FileInputStream(file));){ - MD5InputStreamWrapper md5stream = null; - try { - md5stream = new MD5InputStreamWrapper(inFileStream); - } catch (NoSuchAlgorithmException e1) { - ErrorHandler.error("Failed to get MD5 hash stream.", e1, true); - } - BufferedReader in = new BufferedReader(new InputStreamReader(md5stream!=null?md5stream:inFileStream, "UTF-8")); - + try ( + InputStream bis = new BufferedInputStream(new FileInputStream(file)); + MD5InputStreamWrapper md5stream = (!hasNoMD5Algorithm) ? new MD5InputStreamWrapper(bis) : null; + BufferedReader in = new BufferedReader(new InputStreamReader((md5stream != null) ? md5stream : bis, "UTF-8")); + ) { String line = in.readLine(); String tokens[] = null; while (line != null) { @@ -594,6 +593,12 @@ public class BeatmapParser { beatmap.md5Hash = md5stream.getMD5(); } catch (IOException e) { ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false); + } catch (NoSuchAlgorithmException e) { + ErrorHandler.error("Failed to get MD5 hash stream.", e, true); + + // retry without MD5 + hasNoMD5Algorithm = true; + return parseFile(file, dir, beatmaps, parseObjects); } // no associated audio file? diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java index 80362838..c146ee67 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java @@ -58,10 +58,9 @@ public class BeatmapSetList { /** Set of all beatmap set IDs for the parsed beatmaps. */ private HashSet MSIDdb; - - /** Map of all hash to Beatmap . */ - public HashMap beatmapHashesToFile; + /** Map of all MD5 hashes to beatmaps. */ + private HashMap beatmapHashDB; /** Index of current expanded node (-1 if no node is expanded). */ private int expandedIndex; @@ -88,7 +87,7 @@ public class BeatmapSetList { private BeatmapSetList() { parsedNodes = new ArrayList(); MSIDdb = new HashSet(); - beatmapHashesToFile = new HashMap(); + beatmapHashDB = new HashMap(); reset(); } @@ -123,10 +122,12 @@ public class BeatmapSetList { int msid = beatmaps.get(0).beatmapSetID; if (msid > 0) MSIDdb.add(msid); - for(Beatmap f : beatmaps) { - beatmapHashesToFile.put(f.md5Hash, f); - } + // add MD5 hashes to table + for (Beatmap beatmap : beatmaps) { + if (beatmap.md5Hash != null) + beatmapHashDB.put(beatmap.md5Hash, beatmap); + } return node; } @@ -511,8 +512,13 @@ public class BeatmapSetList { * @return true if id is in the list */ public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); } - - public Beatmap getFileFromBeatmapHash(String beatmapHash) { - return beatmapHashesToFile.get(beatmapHash); + + /** + * Returns the beatmap associated with the given hash. + * @param beatmapHash the MD5 hash + * @return the associated beatmap, or {@code null} if no match was found + */ + public Beatmap getBeatmapFromHash(String beatmapHash) { + return beatmapHashDB.get(beatmapHash); } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/db/BeatmapDB.java b/src/itdelatrisu/opsu/db/BeatmapDB.java index 57508975..8d47669c 100644 --- a/src/itdelatrisu/opsu/db/BeatmapDB.java +++ b/src/itdelatrisu/opsu/db/BeatmapDB.java @@ -122,7 +122,7 @@ public class BeatmapDB { "bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " + "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " + "mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " + - "bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT," + + "bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " + "md5hash TEXT" + "); " + "CREATE TABLE IF NOT EXISTS info (" + @@ -483,7 +483,7 @@ public class BeatmapDB { if (bg != null) beatmap.bg = new File(dir, BeatmapParser.getDBString(bg)); beatmap.sliderBorderFromString(rs.getString(37)); - beatmap.md5Hash = BeatmapParser.getDBString(rs.getString(41)); + beatmap.md5Hash = rs.getString(41); } catch (SQLException e) { throw e; } catch (Exception e) { diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java index 4feee2da..42ffe50d 100644 --- a/src/itdelatrisu/opsu/db/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -97,12 +97,9 @@ public class ScoreDB { // prepare sql statements try { - - //TODO timestamp as primary key should prevent importing the same replay multiple times - //but if for some magical reason two different replays has the same time stamp - //it will fail, such as copying replays from another drive? which will reset - //the last modified of the file. insertStmt = connection.prepareStatement( + // TODO: There will be problems if multiple replays have the same + // timestamp (e.g. when imported) due to timestamp being the primary key. "INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ); selectMapStmt = connection.prepareStatement( @@ -145,7 +142,7 @@ public class ScoreDB { "combo INTEGER, " + "perfect BOOLEAN, " + "mods INTEGER, " + - "replay TEXT," + + "replay TEXT, " + "playerName TEXT"+ ");" + "CREATE TABLE IF NOT EXISTS info (" + @@ -294,7 +291,6 @@ public class ScoreDB { stmt.setInt(17, data.mods); stmt.setString(18, data.replayString); stmt.setString(19, data.playerName); - } /** diff --git a/src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java b/src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java new file mode 100644 index 00000000..9e2602b0 --- /dev/null +++ b/src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java @@ -0,0 +1,118 @@ +/* + * 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.io; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Wrapper for an InputStream that computes the MD5 hash while reading the stream. + */ +public class MD5InputStreamWrapper extends InputStream { + /** The input stream. */ + private InputStream in; + + /** Whether the end of stream has been reached. */ + private boolean eof = false; + + /** A MessageDigest object that implements the MD5 digest algorithm. */ + private MessageDigest md; + + /** The computed MD5 hash. */ + private String md5; + + /** + * Constructor. + * @param in the input stream + * @throws NoSuchAlgorithmException if no Provider supports a MessageDigestSpi implementation for the MD5 algorithm + */ + public MD5InputStreamWrapper(InputStream in) throws NoSuchAlgorithmException { + this.in = in; + this.md = MessageDigest.getInstance("MD5"); + } + + @Override + public int read() throws IOException { + int bytesRead = in.read(); + if (bytesRead >= 0) + md.update((byte) bytesRead); + else + eof = true; + return bytesRead; + } + + @Override + public int available() throws IOException { return in.available(); } + + @Override + public void close() throws IOException { in.close(); } + + @Override + public synchronized void mark(int readlimit) { in.mark(readlimit); } + + @Override + public boolean markSupported() { return in.markSupported(); } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int bytesRead = in.read(b, off, len); + if (bytesRead >= 0) + md.update(b, off, bytesRead); + else + eof = true; + return bytesRead; + } + + @Override + public int read(byte[] b) throws IOException { return read(b, 0, b.length); } + + @Override + public synchronized void reset() throws IOException { + throw new RuntimeException("The reset() method is not implemented."); + } + + @Override + public long skip(long n) throws IOException { + throw new RuntimeException("The skip() method is not implemented."); + } + + /** + * Returns the MD5 hash of the input stream. + * @throws IOException if the end of stream has not yet been reached and a call to {@link #read(byte[])} fails + */ + public String getMD5() throws IOException { + if (md5 != null) + return md5; + + if (!eof) { // read the rest of the stream + byte[] buf = new byte[0x1000]; + while (!eof) + read(buf); + } + + byte[] md5byte = md.digest(); + StringBuilder result = new StringBuilder(); + for (byte b : md5byte) + result.append(String.format("%02x", b)); + md5 = result.toString(); + return md5; + } +} diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index c844dcc0..5bc1681c 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -38,9 +38,9 @@ public class Circle implements GameObject { /** The amount of time, in milliseconds, to fade in the circle. */ private static final int FADE_IN_TIME = 375; - /** The diameter of Circle Hitobjects */ + /** The diameter of hit circles. */ private static float diameter; - + /** The associated HitObject. */ private HitObject hitObject; @@ -65,9 +65,9 @@ public class Circle implements GameObject { * @param circleSize the map's circleSize value */ public static void init(GameContainer container, float circleSize) { - diameter = (104 - (circleSize * 8)); - diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) - int diameterInt = (int)diameter; + diameter = (104 - (circleSize * 8)); + diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) + int diameterInt = (int) diameter; GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); @@ -123,7 +123,6 @@ public class Circle implements GameObject { private int hitResult(int time) { int timeDiff = Math.abs(time); - int[] hitResultOffset = game.getHitResultOffsets(); int result = -1; if (timeDiff <= hitResultOffset[GameData.HIT_300]) @@ -142,13 +141,13 @@ public class Circle implements GameObject { @Override public boolean mousePressed(int x, int y, int trackPosition) { double distance = Math.hypot(this.x - x, this.y - y); - if (distance < diameter/2) { + if (distance < diameter / 2) { int timeDiff = trackPosition - hitObject.getTime(); int result = hitResult(timeDiff); if (result > -1) { data.addHitError(hitObject.getTime(), x, y, timeDiff); - data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true); + data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false); return true; } } @@ -164,17 +163,17 @@ public class Circle implements GameObject { if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { if (isAutoMod) // "auto" mod: catch any missed notes due to lag - data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false); else // no more points can be scored, so send a miss - data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true); + data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false); return true; } // "auto" mod: send a perfect hit result else if (isAutoMod) { if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) { - data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true); + data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false); return true; } } @@ -199,7 +198,5 @@ public class Circle implements GameObject { } @Override - public void reset() { - - } + public void reset() {} } diff --git a/src/itdelatrisu/opsu/objects/DummyObject.java b/src/itdelatrisu/opsu/objects/DummyObject.java index 80f920a7..45c9c1ed 100644 --- a/src/itdelatrisu/opsu/objects/DummyObject.java +++ b/src/itdelatrisu/opsu/objects/DummyObject.java @@ -65,7 +65,5 @@ public class DummyObject implements GameObject { } @Override - public void reset() { - - } + public void reset() {} } diff --git a/src/itdelatrisu/opsu/objects/GameObject.java b/src/itdelatrisu/opsu/objects/GameObject.java index 39c66020..2a40ca47 100644 --- a/src/itdelatrisu/opsu/objects/GameObject.java +++ b/src/itdelatrisu/opsu/objects/GameObject.java @@ -69,11 +69,9 @@ public interface GameObject { * Updates the position of the hit object. */ public void updatePosition(); - + /** - * Resets the hit object so that it can be reused. + * Resets all internal state so that the hit object can be reused. */ public void reset(); - - } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index ee81fb77..2ba83c71 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -49,9 +49,11 @@ public class Slider implements GameObject { /** Rate at which slider ticks are placed. */ private static float sliderTickRate = 1.0f; - + + /** Follow circle radius. */ private static float followRadius; - + + /** The diameter of hit circles. */ private static float diameter; /** The amount of time, in milliseconds, to fade in the slider. */ @@ -81,8 +83,11 @@ public class Slider implements GameObject { /** The time duration of the slider including repeats, in milliseconds. */ private float sliderTimeTotal = 0f; - /** Whether or not the result of the initial/final hit circles have been processed. */ - private boolean sliderClickedInitial = false, sliderClickedFinal = false; + /** Whether or not the result of the initial hit circle has been processed. */ + private boolean sliderClickedInitial = false; + + /** Whether or not the slider was held to the end. */ + private boolean sliderHeldToEnd = false; /** Whether or not to show the follow circle. */ private boolean followCircleActive = false; @@ -104,8 +109,6 @@ public class Slider implements GameObject { /** Container dimensions. */ private static int containerWidth, containerHeight; - - /** * Initializes the Slider data type with images and dimensions. @@ -116,12 +119,13 @@ public class Slider implements GameObject { public static void init(GameContainer container, float circleSize, Beatmap beatmap) { containerWidth = container.getWidth(); containerHeight = container.getHeight(); - - diameter = (104 - (circleSize * 8)); - diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) - int diameterInt = (int)diameter; - + + diameter = (104 - (circleSize * 8)); + diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) + int diameterInt = (int) diameter; + followRadius = diameter / 2 * 3f; + // slider ball if (GameImage.SLIDER_BALL.hasSkinImages() || (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) @@ -283,51 +287,49 @@ public class Slider implements GameObject { /* time scoredelta score-hit-initial-tick= unaccounted (1/4 - 1) 396 - 300 - 30 46 - (1+1/4 - 2) 442 - 300 - 30 - 10 + (1+1/4 - 2) 442 - 300 - 30 - 10 (2+1/4 - 3) 488 - 300 - 30 - 2*10 896 (408)5x - (3+1/4 - 4) 534 - 300 - 30 - 3*10 - (4+1/4 - 5) 580 - 300 - 30 - 4*10 - (5+1/4 - 6) 626 - 300 - 30 - 5*10 - (6+1/4 - 7) 672 - 300 - 30 - 6*10 - + (3+1/4 - 4) 534 - 300 - 30 - 3*10 + (4+1/4 - 5) 580 - 300 - 30 - 4*10 + (5+1/4 - 6) 626 - 300 - 30 - 5*10 + (6+1/4 - 7) 672 - 300 - 30 - 6*10 + difficultyMulti = 3 (+36 per combo) - - score = + + score = (t)ticks(10) * nticks + - (h)hitValue - (c)combo (hitValue/25 * difficultyMultiplier*(combo-1)) + (h)hitValue + (c)combo (hitValue/25 * difficultyMultiplier*(combo-1)) (i)initialHit (30) + (f)finalHit(30) + - + s t h c i f 626 - 10*5 - 300 - 276(-216 - 30 - 30) (all)(7x) 240 - 10*5 - 100 - 90 (-60 <- 30>) (no final or initial)(6x) - + 218 - 10*4 - 100 - 78 (-36 - 30) (4 tick no initial)(5x) 196 - 10*3 - 100 - 66 (-24 - 30 ) (3 tick no initial)(4x) 112 - 10*2 - 50 - 42 (-12 - 30 ) (2 tick no initial)(3x) 96 - 10 - 50 - 36 ( -6 - 30 ) (1 tick no initial)(2x) - + 206 - 10*4 - 100 - 66 (-36 - 30 ) (4 tick no initial)(4x) 184 - 10*3 - 100 - 54 (-24 - 30 ) (3 tick no initial)(3x) 90 - 10 - 50 - 30 ( - 30 ) (1 tick no initial)(0x) - + 194 - 10*4 - 100 - 54 (-24 - 30 ) (4 tick no initial)(3x) - + 170 - 10*4 - 100 - 30 ( - 30 ) (4 tick no final)(0x) 160 - 10*3 - 100 - 30 ( - 30 ) (3 tick no final)(0x) 100 - 10*2 - 50 - 30 ( - 30 ) (2 tick no final)(0x) - + 198 - 10*5 - 100 - 48 (-36 ) (no initial and final)(5x) 110 - 50 - ( - 30 - 30 ) (final and initial no tick)(0x) 80 - 50 - ( <- 30> ) (only final or initial)(0x) - + 140 - 10*4 - 100 - 0 (4 ticks only)(0x) 80 - 10*3 - 50 - 0 (3 tick only)(0x) 70 - 10*2 - 50 - 0 (2 tick only)(0x) 60 - 10 - 50 - 0 (1 tick only)(0x) - - */ float tickRatio = (float) ticksHit / tickIntervals; @@ -354,7 +356,8 @@ public class Slider implements GameObject { type = HitObjectType.SLIDER_FIRST; } data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, - cx, cy, color, comboEnd, hitObject, currentRepeats + 1, type, curve, sliderClickedFinal); + cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd, + currentRepeats + 1, curve, sliderHeldToEnd); return result; } @@ -429,21 +432,17 @@ public class Slider implements GameObject { float[] c = curve.pointAt(getT(trackPosition, false)); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); if (distance < followRadius) - sliderClickedFinal = true; + sliderHeldToEnd = true; } // final circle hit - if (sliderClickedFinal){ + if (sliderHeldToEnd) ticksHit++; - data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats); - } // "auto" mod: always send a perfect hit result if (isAutoMod) ticksHit = tickIntervals; - //TODO missing the final shouldn't increment the combo - // calculate and send slider result hitResult(); return true; @@ -501,8 +500,8 @@ public class Slider implements GameObject { } // held near end of slider - if (!sliderClickedFinal && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300]) - sliderClickedFinal = true; + if (!sliderHeldToEnd && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300]) + sliderHeldToEnd = true; } else { followCircleActive = false; @@ -563,12 +562,11 @@ public class Slider implements GameObject { @Override public void reset() { sliderClickedInitial = false; - sliderClickedFinal = false; + sliderHeldToEnd = false; followCircleActive = false; currentRepeats = 0; tickIndex = 0; ticksHit = 0; tickIntervals = 1; } - } diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 9f0c18de..3f9cdcab 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -58,13 +58,14 @@ public class Spinner implements GameObject { AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI; SPUN_OUT_MULTIPLIER = 1 / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI; + /** Maximum angle difference. */ + private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3 + /** PI constants. */ private static final float TWO_PI = (float) (Math.PI * 2), HALF_PI = (float) (Math.PI / 2); - private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3 - /** The associated HitObject. */ private HitObject hitObject; @@ -97,10 +98,10 @@ public class Spinner implements GameObject { /** Current index of the stored velocities in rotations/second. */ private int deltaAngleIndex = 0; - + /** The remaining amount of the angle that was not used. */ private float deltaAngleOverflow = 0; - + /** The RPM that is drawn to the screen. */ private int drawnRPM = 0; @@ -124,46 +125,47 @@ public class Spinner implements GameObject { public Spinner(HitObject hitObject, Game game, GameData data) { this.hitObject = hitObject; this.data = data; + /* 1 beat = 731.707317073171ms RPM at frame X with spinner Y beats long 10 20 30 40 50 60 5sec ~ 48 ~ 800ms + // TODO not correct at all, but close enough? + // <2sec ~ 12 ~ 200ms + // >5sec ~ 48 ~ 800ms + final int minVel = 12; final int maxVel = 48; final int minTime = 2000; final int maxTime = 5000; - maxStoredDeltaAngles = (int) Utils.clamp( - (hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel-minVel)/(maxTime-minTime) + minVel - , minVel, maxVel); + maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) + * (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel); storedDeltaAngle = new float[maxStoredDeltaAngles]; - + // calculate rotations needed float spinsPerMinute = 100 + (overallDifficulty * 15); rotationsNeeded = spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f; @@ -245,19 +247,18 @@ public class Spinner implements GameObject { result = GameData.HIT_MISS; data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2, - Color.transparent, true, hitObject, 0, HitObjectType.SPINNER, null, true); + Color.transparent, true, hitObject, HitObjectType.SPINNER, true, 0, null, false); return result; } @Override - public boolean mousePressed(int x, int y, int trackPosition) { + public boolean mousePressed(int x, int y, int trackPosition) { lastAngle = (float) Math.atan2(x - (height / 2), y - (width / 2)); return false; } @Override public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) { - // end of spinner if (overlap || trackPosition > hitObject.getEndTime()) { hitResult(); @@ -270,9 +271,9 @@ public class Spinner implements GameObject { // spin automatically // http://osu.ppy.sh/wiki/FAQ#Spinners - + deltaOverflow += delta; - + float angleDiff = 0; if (GameMod.AUTO.isActive()) { angleDiff = delta * AUTO_MULTIPLIER; @@ -289,13 +290,12 @@ public class Spinner implements GameObject { isSpinning = true; return false; } + angleDiff = angle - lastAngle; - if(Math.abs(angleDiff) > 0.01f){ + if (Math.abs(angleDiff) > 0.01f) lastAngle = angle; - }else{ + else angleDiff = 0; - } - } // make angleDiff the smallest angle change possible @@ -304,36 +304,36 @@ public class Spinner implements GameObject { angleDiff += TWO_PI; else if (angleDiff > Math.PI) angleDiff -= TWO_PI; - - //may be a problem at higher frame rate due to float point round off + + // may be a problem at higher frame rate due to floating point round off if (isSpinning) deltaAngleOverflow += angleDiff; - + while (deltaOverflow >= DELTA_UPDATE_TIME) { // spin caused by the cursor - float deltaAngle = 0; - if (isSpinning){ + float deltaAngle = 0; + if (isSpinning) { deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow; deltaAngleOverflow -= deltaAngle; - deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); + deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); } sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex]; sumDeltaAngle += deltaAngle; storedDeltaAngle[deltaAngleIndex++] = deltaAngle; deltaAngleIndex %= storedDeltaAngle.length; deltaOverflow -= DELTA_UPDATE_TIME; - - float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles; - rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); - float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI; - drawnRPM = (int)(Math.abs(rotationPerSec * 60)); - + float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles; + rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); + float rotationPerSec = rotationAngle * (1000 / DELTA_UPDATE_TIME) / TWO_PI; + + drawnRPM = (int) (Math.abs(rotationPerSec * 60)); + rotate(rotationAngle); if (Math.abs(rotationAngle) > 0.00001f) data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER); - } + //TODO may need to update 1 more time when the spinner ends? return false; } @@ -380,21 +380,17 @@ public class Spinner implements GameObject { //TODO seems to give 1100 points per spin but also an extra 100 for some spinners if (newRotations > rotationsNeeded) { // extra rotations data.changeScore(1000); - SoundController.playSound(SoundEffect.SPINNERBONUS); } data.changeScore(100); SoundController.playSound(SoundEffect.SPINNERSPIN); - } - /* - //The extra 100 for some spinners (mostly wrong) - if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) { - if (newRotations + 0.5f > rotationsNeeded) { // extra rotations - data.changeScore(100); - } - } - //*/ + + // extra 100 for some spinners (mostly wrong) +// if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) { +// if (newRotations + 0.5f > rotationsNeeded) // extra rotations +// data.changeScore(100); +// } rotations = newRotations; } @@ -403,9 +399,8 @@ public class Spinner implements GameObject { public void reset() { deltaAngleIndex = 0; sumDeltaAngle = 0; - for(int i=0; i. */ @@ -77,7 +77,7 @@ public class CurveRenderState { } /** - * Undo the static state. Static state setup caused by calls to + * Undo the static state. Static state setup caused by calls to * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, itdelatrisu.opsu.objects.curves.Vec2f[])} * are undone. */ @@ -280,7 +280,7 @@ public class CurveRenderState { for (int i = 0; i < curve.length * 2 - 1; ++i) GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); GL20.glDisableVertexAttribArray(staticState.texCoordLoc); - GL20.glDisableVertexAttribArray(staticState.attribLoc); + GL20.glDisableVertexAttribArray(staticState.attribLoc); GL15.glDeleteBuffers(vtx_buf); endRender(state); } @@ -335,7 +335,7 @@ public class CurveRenderState { buff.put(1f); //GL11.glVertex4f(x + 90 * (float) Math.sin(0.0), y + 90 * (float) Math.cos(0.0), 1.0f, 1.0f); } - + /** * Contains all the necessary state that needs to be tracked to draw curves * in the new style and not re-create the shader each time. @@ -360,16 +360,16 @@ public class CurveRenderState { /** OpenGL shader uniform location of the color attribute. */ protected int colLoc = 0; - + /** OpenGL shader uniform location of the border color attribute. */ protected int colBorderLoc = 0; - + /** OpenGL shader uniform location of the texture sampler attribute. */ protected int texLoc = 0; /** OpenGL texture id for the gradient texture for the curve. */ protected int gradientTexture = 0; - + /** * Reads the first row of the slider gradient texture and upload it as * a 1D texture to OpenGL if it hasn't already been done. diff --git a/src/itdelatrisu/opsu/render/FrameBufferCache.java b/src/itdelatrisu/opsu/render/FrameBufferCache.java index c4d94a5b..c27a80cb 100644 --- a/src/itdelatrisu/opsu/render/FrameBufferCache.java +++ b/src/itdelatrisu/opsu/render/FrameBufferCache.java @@ -1,17 +1,17 @@ /* * 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 . */ @@ -90,7 +90,7 @@ public class FrameBufferCache { } /** - * Clear the cache of all the mappings. This does not actually delete the + * Clear the cache of all the mappings. This does not actually delete the * cached framebuffers, it merely frees them all up to get mapped anew. */ public void freeMap() { @@ -98,8 +98,8 @@ public class FrameBufferCache { } /** - * Create a mapping from {@code obj} to a framebuffer. If there was already - * a mapping from {@code obj} this will associate another framebuffer with it + * Create a mapping from {@code obj} to a framebuffer. If there was already + * a mapping from {@code obj} this will associate another framebuffer with it * (thereby freeing up the previously mapped framebuffer). * @param obj the hit object * @return the {@code Rendertarget} newly mapped to {@code obj} diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index df50c73f..c6f19387 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -1,23 +1,24 @@ /* * 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.render; import java.nio.ByteBuffer; + import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL30; diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java index 13ffa247..505ba128 100644 --- a/src/itdelatrisu/opsu/replay/Replay.java +++ b/src/itdelatrisu/opsu/replay/Replay.java @@ -56,6 +56,9 @@ public class Replay { /** The associated file. */ private File file; + /** The associated score data. */ + private ScoreData scoreData; + /** Whether or not the replay data has been loaded from the file. */ public boolean loaded = false; @@ -104,8 +107,6 @@ public class Replay { /** Seed. (?) */ public int seed; - private ScoreData scoreData; - /** Seed string. */ private static final String SEED_STRING = "-12345"; @@ -136,46 +137,16 @@ public class Replay { reader.close(); loaded = true; } - + + /** + * Loads the replay header only. + * @throws IOException failure to load the data + */ public void loadHeader() throws IOException { OsuReader reader = new OsuReader(file); loadHeader(reader); reader.close(); } - /** - * 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(Beatmap osu) { - if (scoreData != null) - return scoreData; - - scoreData = new ScoreData(); - scoreData.timestamp = file.lastModified() / 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 = hit300; - scoreData.hit100 = hit100; - scoreData.hit50 = hit50; - scoreData.geki = geki; - scoreData.katu = katu; - scoreData.miss = miss; - scoreData.score = score; - scoreData.combo = combo; - scoreData.perfect = perfect; - scoreData.mods = mods; - scoreData.replayString = file!=null ? file.getName() : getReplayFilename(); - scoreData.playerName = playerName!=null ? playerName : "No Name"; - return scoreData; - } - /** * Loads the replay header data. @@ -260,6 +231,40 @@ public class Replay { } } + /** + * Returns a ScoreData object encapsulating all replay data. + * If score data already exists, the existing object will be returned + * (i.e. this will not overwrite existing data). + * @param beatmap the beatmap + * @return the ScoreData object + */ + public ScoreData getScoreData(Beatmap beatmap) { + if (scoreData != null) + return scoreData; + + scoreData = new ScoreData(); + scoreData.timestamp = file.lastModified() / 1000L; + scoreData.MID = beatmap.beatmapID; + scoreData.MSID = beatmap.beatmapSetID; + scoreData.title = beatmap.title; + scoreData.artist = beatmap.artist; + scoreData.creator = beatmap.creator; + scoreData.version = beatmap.version; + scoreData.hit300 = hit300; + scoreData.hit100 = hit100; + scoreData.hit50 = hit50; + scoreData.geki = geki; + scoreData.katu = katu; + scoreData.miss = miss; + scoreData.score = score; + scoreData.combo = combo; + scoreData.perfect = perfect; + scoreData.mods = mods; + scoreData.replayString = getReplayFilename(); + scoreData.playerName = playerName; + return scoreData; + } + /** * Saves the replay data to a file in the replays directory. */ diff --git a/src/itdelatrisu/opsu/replay/ReplayImporter.java b/src/itdelatrisu/opsu/replay/ReplayImporter.java index 8fd27b2c..06d5a3df 100644 --- a/src/itdelatrisu/opsu/replay/ReplayImporter.java +++ b/src/itdelatrisu/opsu/replay/ReplayImporter.java @@ -1,49 +1,119 @@ +/* + * 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.replay; import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; -import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.db.ScoreDB; import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; -import org.newdawn.slick.util.Log; - +/** + * Importer for replay files. + */ public class ReplayImporter { + /** The index of the current file being imported. */ + private static int fileIndex = -1; + + /** The total number of replays to import. */ + private static File[] files; + + // This class should not be instantiated. + private ReplayImporter() {} + + /** + * Invokes the importer for each OSR file in a directory, adding the replay + * to the score database and moving the file into the replay directory. + * @param dir the directory + */ public static void importAllReplaysFromDir(File dir) { - for (File replayToImport : dir.listFiles()) { - try { - Replay r = new Replay(replayToImport); - r.loadHeader(); - Beatmap oFile = BeatmapSetList.get().getFileFromBeatmapHash(r.beatmapHash); - if(oFile != null){ - File replaydir = Options.getReplayDir(); - if (!replaydir.isDirectory()) { - if (!replaydir.mkdir()) { - ErrorHandler.error("Failed to create replay directory.", null, false); - return; - } - } - //ErrorHandler.error("Importing"+replayToImport+" forBeatmap:"+oFile, null, false); - ScoreData data = r.getScoreData(oFile); - File moveToFile = new File(replaydir, replayToImport.getName()); - if( - !replayToImport.renameTo(moveToFile) - ){ - Log.warn("Rename Failed "+moveToFile); - } - data.replayString = replayToImport.getName().substring(0, replayToImport.getName().length()-4); - ScoreDB.addScore(data);; - } else { - Log.warn("Could not find beatmap for replay "+replayToImport); - } - } catch (IOException e) { - Log.warn("Failed to import replays ",e); + // find all OSR files + files = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".osr"); } - + }); + if (files == null || files.length < 1) { + files = null; + return; } + + // get replay directory + File replayDir = Options.getReplayDir(); + if (!replayDir.isDirectory()) { + if (!replayDir.mkdir()) { + ErrorHandler.error(String.format("Failed to create replay directory '%s'.", replayDir.getAbsolutePath()), null, false); + return; + } + } + + // import OSRs + for (File file : files) { + fileIndex++; + Replay r = new Replay(file); + try { + r.loadHeader(); + } catch (IOException e) { + ErrorHandler.error(String.format("Failed to import replay '%s'. The replay file could not be parsed.", file.getName()), e, false); + continue; + } + Beatmap beatmap = BeatmapSetList.get().getBeatmapFromHash(r.beatmapHash); + if (beatmap != null) { + File moveToFile = new File(replayDir, String.format("%s.osr", r.getReplayFilename())); + if (!file.renameTo(moveToFile)) { + ErrorHandler.error(String.format("Failed to import replay '%s'. The replay file could not be moved to the replay directory.", file.getName()), null, false); + //continue; + } + ScoreDB.addScore(r.getScoreData(beatmap)); + } else { + ErrorHandler.error(String.format("Failed to import replay '%s'. The associated beatmap could not be found.", file.getName()), null, false); + continue; + } + } + + fileIndex = -1; + files = null; + } + + /** + * Returns the name of the current file being imported, or null if none. + */ + public static String getCurrentFileName() { + if (files == null || fileIndex == -1) + return null; + + return files[fileIndex].getName(); + } + + /** + * Returns the progress of replay importing, or -1 if not importing. + * @return the completion percent [0, 100] or -1 + */ + public static int getLoadingProgress() { + if (files == null || fileIndex == -1) + return -1; + + return (fileIndex + 1) * 100 / files.length; } } diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 98c08d63..7ade156f 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -26,9 +26,9 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetNode; -import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadNode; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index cfb7976f..2c46797f 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -217,18 +217,15 @@ public class Game extends BasicGameState { /** Playback speed (used in replays and "auto" mod). */ private PlaybackSpeed playbackSpeed; + /** Whether the game is currently seeking to a replay position. */ + private boolean isSeeking; + // game-related variables private GameContainer container; private StateBasedGame game; private Input input; private int state; - private int width; - - private int height; - - private boolean seeking; - public Game(int state) { this.state = state; } @@ -240,8 +237,8 @@ public class Game extends BasicGameState { this.game = game; input = container.getInput(); - width = container.getWidth(); - height = container.getHeight(); + int width = container.getWidth(); + int height = container.getHeight(); // create offscreen graphics offscreen = new Image(width, height); @@ -615,15 +612,16 @@ public class Game extends BasicGameState { // out of frames, use previous data if (replayIndex >= replay.frames.length) updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed); - + //TODO probably should to disable sounds then reseek to the new position - if(seeking && replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){ + if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length && + trackPosition < replay.frames[replayIndex - 1].getTime()) { replayIndex = 0; - while(objectIndex>=0){ + while (objectIndex >= 0) { gameObjects[objectIndex].reset(); objectIndex--; - } + // reset game data resetGameData(); @@ -635,7 +633,7 @@ public class Game extends BasicGameState { timingPointIndex++; } } - seeking = false; + isSeeking = false; } // update and run replay frames @@ -925,10 +923,11 @@ public class Game extends BasicGameState { MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier()); } - if(!GameMod.AUTO.isActive() && y < 50){ - float pos = (float)x / width * beatmap.endTime; - MusicController.setPosition((int)pos); - seeking = true; + // TODO + else if (!GameMod.AUTO.isActive() && y < 50) { + float pos = (float) x / container.getWidth() * beatmap.endTime; + MusicController.setPosition((int) pos); + isSeeking = true; } return; } @@ -1338,7 +1337,7 @@ public class Game extends BasicGameState { autoMouseY = 0; autoMousePressed = false; flashlightRadius = container.getHeight() * 2 / 3; - + System.gc(); } @@ -1439,19 +1438,16 @@ public class Game extends BasicGameState { // overallDifficulty (hit result time offsets) hitResultOffset = new int[GameData.HIT_MAX]; - /* - float mult = 0.608f; - hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6))*mult); - hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8))*mult); - hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16))*mult); - hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10))*mult); - /*/ hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6)); hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8)); hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10)); hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10)); + //final float mult = 0.608f; + //hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult); + //hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8)) * mult); + //hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16)) * mult); + //hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10)) * mult); data.setHitResultOffset(hitResultOffset); - //*/ // HPDrainRate (health change) data.setDrainRate(HPDrainRate); diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 53aa372e..be484887 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -32,8 +32,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; -import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.MenuButton.Expand; +import itdelatrisu.opsu.ui.UI; import java.awt.Desktop; import java.io.IOException; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 4e9cc4e6..d191d125 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -32,10 +32,10 @@ import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.beatmap.BeatmapSortOrder; -import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index 8d8ef388..14d8245a 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -25,9 +25,9 @@ import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; -import itdelatrisu.opsu.replay.ReplayImporter; -import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapParser; +import itdelatrisu.opsu.beatmap.BeatmapSetList; +import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.ui.UI; import java.io.File; @@ -128,7 +128,7 @@ public class Splash extends BasicGameState { // parse song directory BeatmapParser.parseAllFiles(beatmapDir); - + // import replays ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir()); diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 80e4cd3b..91ee4c9a 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -25,6 +25,7 @@ import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; +import itdelatrisu.opsu.replay.ReplayImporter; import javax.swing.JOptionPane; import javax.swing.UIManager; @@ -285,6 +286,9 @@ public class UI { text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ? "Updating database..." : "Loading beatmaps..."; progress = BeatmapParser.getParserProgress(); + } else if ((file = ReplayImporter.getCurrentFileName()) != null) { + text = "Importing replays..."; + progress = ReplayImporter.getLoadingProgress(); } else if ((file = SoundController.getCurrentFileName()) != null) { text = "Loading sounds..."; progress = SoundController.getLoadingProgress(); diff --git a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java index 7042407b..a6e6c3f4 100644 --- a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java +++ b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java @@ -41,7 +41,6 @@ import org.lwjgl.openal.OpenALException; import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; - /** * A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL * as required. @@ -356,7 +355,7 @@ public class OpenALStreamPlayer { } playedPos = streamPos; - syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate)/pitch); + syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate) / pitch); startPlayback(); @@ -409,28 +408,30 @@ public class OpenALStreamPlayer { // hard reset if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) { - //System.out.println("Time HARD Reset"+" "+thisPosition+" "+(dxTime / 1000f)); syncPosition(); dxTime = (thisTime - syncStartTime) * pitch; avgDiff = 0; } if ((int) (dxPosition * 1000) != 0) { // lastPosition != thisPosition float diff = thisPosition * 1000 - (dxTime); - + avgDiff = (diff + avgDiff * 9) / 10; - if(Math.abs(avgDiff) >= 1){ - syncStartTime -= (int)(avgDiff); - avgDiff -= (int)(avgDiff); + if (Math.abs(avgDiff) >= 1) { + syncStartTime -= (int) (avgDiff); + avgDiff -= (int) (avgDiff); dxTime = (thisTime - syncStartTime) * pitch; } lastUpdatePosition = thisPosition; } - return dxTime / 1000f; } - private void syncPosition(){ - syncStartTime = getTime() - (long) ( getALPosition() * 1000 / pitch); + + /** + * Synchronizes the track position. + */ + private void syncPosition() { + syncStartTime = getTime() - (long) (getALPosition() * 1000 / pitch); avgDiff = 0; }