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 <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-06-29 19:22:38 -05:00
parent 7d08a7d391
commit d860a30aed
29 changed files with 513 additions and 426 deletions

View File

@ -74,7 +74,7 @@ public class ErrorHandler {
/** /**
* Displays an error popup and logs the given error. * 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 e the exception causing the error
* @param report whether to ask to report the error * @param report whether to ask to report the error
*/ */

View File

@ -838,8 +838,9 @@ public class GameData {
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white); String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6, Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", beatmap.creator), Color.white); 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, 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 // mod icons
int modWidth = GameMod.AUTO.getImage().getWidth(); int modWidth = GameMod.AUTO.getImage().getWidth();
@ -1207,13 +1208,8 @@ public class GameData {
} }
fullObjectCount++; 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: * Returns the score for a hit based on the following score formula:
* <p> * <p>
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25 * Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
@ -1224,26 +1220,31 @@ public class GameData {
* <li><strong>Mod:</strong> mod multipliers * <li><strong>Mod:</strong> mod multipliers
* </ul> * </ul>
* @param hitValue the hit value * @param hitValue the hit value
* @param hitObject * @param hitObject the hit object
* @return the score value * @return the score value
* @see <a href="https://osu.ppy.sh/wiki/Score">https://osu.ppy.sh/wiki/Score</a>
*/ */
private int getScoreForHit(int hitValue, HitObject hitObject) { private int getScoreForHit(int hitValue, HitObject hitObject) {
int comboMulti = Math.max(combo - 1, 0); int comboMultiplier = Math.max(combo - 1, 0);
if(hitObject.isSlider()){ if (hitObject.isSlider())
comboMulti += 1; comboMultiplier++;
} return (hitValue + (int)(hitValue * (comboMultiplier * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25));
return (hitValue + (int)(hitValue * (comboMulti * 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. * Computes and stores the difficulty multiplier used in the score formula.
* @param drainRate the raw HP drain rate value * @param drainRate the raw HP drain rate value
* @param circleSize the raw circle size value * @param circleSize the raw circle size value
* @param overallDifficulty the raw overall difficulty value * @param overallDifficulty the raw overall difficulty value
* @see <a href="https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier">https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier</a>
*/ */
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) { 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 float sum = drainRate + circleSize + overallDifficulty; // typically 2~27
if (sum <= 5f) if (sum <= 5f)
difficultyMultiplier = 2; difficultyMultiplier = 2;
@ -1255,21 +1256,11 @@ public class GameData {
difficultyMultiplier = 5; difficultyMultiplier = 5;
else //if (sum <= 30f) else //if (sum <= 30f)
difficultyMultiplier = 6; difficultyMultiplier = 6;
//*/
/* //float multiplier = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f;
924 3x1/4 beat notes 0.14stars //difficultyMultiplier = (int) multiplier;
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;
*/
} }
/** /**
* Handles a hit result and performs all associated calculations. * Handles a hit result and performs all associated calculations.
* @param time the object start time * @param time the object start time
@ -1279,12 +1270,13 @@ public class GameData {
* @param color the combo color * @param color the combo color
* @param end true if this is the last hit object in the combo * @param end true if this is the last hit object in the combo
* @param hitObject the hit object * @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 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) * @return the actual hit result (HIT_* constants)
*/ */
private int handleHitResult(int time, int result, float x, float y, Color color, private int handleHitResult(int time, int result, float x, float y, Color color, boolean end,
boolean end, HitObject hitObject, int repeat, HitObjectType hitResultType) { HitObject hitObject, HitObjectType hitResultType, int repeat, boolean noIncrementCombo) {
// update health, score, and combo streak based on hit result // update health, score, and combo streak based on hit result
int hitValue = 0; int hitValue = 0;
switch (result) { switch (result) {
@ -1318,6 +1310,7 @@ public class GameData {
// calculate score and increment combo streak // calculate score and increment combo streak
changeScore(getScoreForHit(hitValue, hitObject)); changeScore(getScoreForHit(hitValue, hitObject));
if (!noIncrementCombo)
incrementComboStreak(); incrementComboStreak();
} }
hitResultCount[result]++; hitResultCount[result]++;
@ -1347,7 +1340,7 @@ public class GameData {
} }
/** /**
* Handles a slider hit result. * Handles a hit result.
* @param time the object start time * @param time the object start time
* @param result the hit result (HIT_* constants) * @param result the hit result (HIT_* constants)
* @param x the x coordinate * @param x the x coordinate
@ -1355,15 +1348,17 @@ public class GameData {
* @param color the combo color * @param color the combo color
* @param end true if this is the last hit object in the combo * @param end true if this is the last hit object in the combo
* @param hitObject the hit object * @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 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 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, public void hitResult(int time, int result, float x, float y, Color color,
boolean end, HitObject hitObject, int repeat, boolean end, HitObject hitObject, HitObjectType hitResultType,
HitObjectType hitResultType, Curve curve, boolean expand) { boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) {
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject, repeat, hitResultType); 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()) if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
; // hide perfect hit results ; // hide perfect hit results
@ -1382,7 +1377,6 @@ public class GameData {
} }
} }
/** /**
* Returns a ScoreData object encapsulating all game data. * Returns a ScoreData object encapsulating all game data.
* If score data already exists, the existing object will be returned * If score data already exists, the existing object will be returned
@ -1413,7 +1407,7 @@ public class GameData {
scoreData.perfect = (comboMax == fullObjectCount); scoreData.perfect = (comboMax == fullObjectCount);
scoreData.mods = GameMod.getModState(); scoreData.mods = GameMod.getModState();
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
scoreData.playerName = "OpsuPlayer"; //TODO GameDataPlayerName? scoreData.playerName = null; // TODO
return scoreData; return scoreData;
} }
@ -1434,7 +1428,7 @@ public class GameData {
replay = new Replay(); replay = new Replay();
replay.mode = Beatmap.MODE_OSU; replay.mode = Beatmap.MODE_OSU;
replay.version = Updater.get().getBuildDate(); 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.playerName = ""; // TODO
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300]; replay.hit300 = (short) hitResultCount[HIT_300];

View File

@ -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();
}
}

View File

@ -33,7 +33,7 @@ public class OszUnpacker {
/** The index of the current file being unpacked. */ /** The index of the current file being unpacked. */
private static int fileIndex = -1; private static int fileIndex = -1;
/** The total number of directories to parse. */ /** The total number of files to unpack. */
private static File[] files; private static File[] files;
// This class should not be instantiated. // This class should not be instantiated.

View File

@ -63,6 +63,9 @@ public class ScoreData implements Comparable<ScoreData> {
/** The replay string. */ /** The replay string. */
public String replayString; public String replayString;
/** The player name. */
public String playerName;
/** Time since the score was achieved. */ /** Time since the score was achieved. */
private String timeSince; private String timeSince;
@ -78,9 +81,6 @@ public class ScoreData implements Comparable<ScoreData> {
/** The tooltip string. */ /** The tooltip string. */
private String tooltip; private String tooltip;
/** The players Name. */
public String playerName;
/** Drawing values. */ /** Drawing values. */
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset; private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
@ -262,9 +262,10 @@ public class ScoreData implements Comparable<ScoreData> {
); );
// hit counts (custom: osu! shows user instead, above score) // hit counts (custom: osu! shows user instead, above score)
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
Utils.FONT_SMALL.drawString( Utils.FONT_SMALL.drawString(
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(), 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 Color.white
); );
@ -335,11 +336,6 @@ public class ScoreData implements Comparable<ScoreData> {
); );
} }
public String getPlayerName() {
if(playerName == null)
return "Null Name";
return playerName;
}
@Override @Override
public int compareTo(ScoreData that) { public int compareTo(ScoreData that) {
if (this.score != that.score) if (this.score != that.score)

View File

@ -185,7 +185,7 @@ public class Beatmap implements Comparable<Beatmap> {
/** Slider border color. If null, the skin value is used. */ /** Slider border color. If null, the skin value is used. */
public Color sliderBorder; public Color sliderBorder;
/** md5 hash of this file */ /** MD5 hash of this file. */
public String md5Hash; public String md5Hash;
/** /**

View File

@ -19,9 +19,9 @@
package itdelatrisu.opsu.beatmap; package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.MD5InputStreamWrapper;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -68,6 +68,9 @@ public class BeatmapParser {
/** The current status. */ /** The current status. */
private static Status status = Status.NONE; 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. // This class should not be instantiated.
private BeatmapParser() {} private BeatmapParser() {}
@ -209,15 +212,11 @@ public class BeatmapParser {
Beatmap beatmap = new Beatmap(file); Beatmap beatmap = new Beatmap(file);
beatmap.timingPoints = new ArrayList<TimingPoint>(); beatmap.timingPoints = new ArrayList<TimingPoint>();
try (InputStream inFileStream = new BufferedInputStream(new FileInputStream(file));){ try (
MD5InputStreamWrapper md5stream = null; InputStream bis = new BufferedInputStream(new FileInputStream(file));
try { MD5InputStreamWrapper md5stream = (!hasNoMD5Algorithm) ? new MD5InputStreamWrapper(bis) : null;
md5stream = new MD5InputStreamWrapper(inFileStream); BufferedReader in = new BufferedReader(new InputStreamReader((md5stream != null) ? md5stream : bis, "UTF-8"));
} 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"));
String line = in.readLine(); String line = in.readLine();
String tokens[] = null; String tokens[] = null;
while (line != null) { while (line != null) {
@ -594,6 +593,12 @@ public class BeatmapParser {
beatmap.md5Hash = md5stream.getMD5(); beatmap.md5Hash = md5stream.getMD5();
} catch (IOException e) { } catch (IOException e) {
ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false); 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? // no associated audio file?

View File

@ -59,9 +59,8 @@ public class BeatmapSetList {
/** Set of all beatmap set IDs for the parsed beatmaps. */ /** Set of all beatmap set IDs for the parsed beatmaps. */
private HashSet<Integer> MSIDdb; private HashSet<Integer> MSIDdb;
/** Map of all hash to Beatmap . */ /** Map of all MD5 hashes to beatmaps. */
public HashMap<String, Beatmap> beatmapHashesToFile; private HashMap<String, Beatmap> beatmapHashDB;
/** Index of current expanded node (-1 if no node is expanded). */ /** Index of current expanded node (-1 if no node is expanded). */
private int expandedIndex; private int expandedIndex;
@ -88,7 +87,7 @@ public class BeatmapSetList {
private BeatmapSetList() { private BeatmapSetList() {
parsedNodes = new ArrayList<BeatmapSetNode>(); parsedNodes = new ArrayList<BeatmapSetNode>();
MSIDdb = new HashSet<Integer>(); MSIDdb = new HashSet<Integer>();
beatmapHashesToFile = new HashMap<String, Beatmap>(); beatmapHashDB = new HashMap<String, Beatmap>();
reset(); reset();
} }
@ -123,10 +122,12 @@ public class BeatmapSetList {
int msid = beatmaps.get(0).beatmapSetID; int msid = beatmaps.get(0).beatmapSetID;
if (msid > 0) if (msid > 0)
MSIDdb.add(msid); 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; return node;
} }
@ -512,7 +513,12 @@ public class BeatmapSetList {
*/ */
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); } 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);
} }
} }

View File

@ -122,7 +122,7 @@ public class BeatmapDB {
"bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " + "bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " + "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " + "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" + "md5hash TEXT" +
"); " + "); " +
"CREATE TABLE IF NOT EXISTS info (" + "CREATE TABLE IF NOT EXISTS info (" +
@ -483,7 +483,7 @@ public class BeatmapDB {
if (bg != null) if (bg != null)
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg)); beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
beatmap.sliderBorderFromString(rs.getString(37)); beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = BeatmapParser.getDBString(rs.getString(41)); beatmap.md5Hash = rs.getString(41);
} catch (SQLException e) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {

View File

@ -97,12 +97,9 @@ public class ScoreDB {
// prepare sql statements // prepare sql statements
try { 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( 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
); );
selectMapStmt = connection.prepareStatement( selectMapStmt = connection.prepareStatement(
@ -145,7 +142,7 @@ public class ScoreDB {
"combo INTEGER, " + "combo INTEGER, " +
"perfect BOOLEAN, " + "perfect BOOLEAN, " +
"mods INTEGER, " + "mods INTEGER, " +
"replay TEXT," + "replay TEXT, " +
"playerName TEXT"+ "playerName TEXT"+
");" + ");" +
"CREATE TABLE IF NOT EXISTS info (" + "CREATE TABLE IF NOT EXISTS info (" +
@ -294,7 +291,6 @@ public class ScoreDB {
stmt.setInt(17, data.mods); stmt.setInt(17, data.mods);
stmt.setString(18, data.replayString); stmt.setString(18, data.replayString);
stmt.setString(19, data.playerName); stmt.setString(19, data.playerName);
} }
/** /**

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -38,7 +38,7 @@ public class Circle implements GameObject {
/** The amount of time, in milliseconds, to fade in the circle. */ /** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375; private static final int FADE_IN_TIME = 375;
/** The diameter of Circle Hitobjects */ /** The diameter of hit circles. */
private static float diameter; private static float diameter;
/** The associated HitObject. */ /** The associated HitObject. */
@ -67,7 +67,7 @@ public class Circle implements GameObject {
public static void init(GameContainer container, float circleSize) { public static void init(GameContainer container, float circleSize) {
diameter = (104 - (circleSize * 8)); diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter; int diameterInt = (int) diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.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) { private int hitResult(int time) {
int timeDiff = Math.abs(time); int timeDiff = Math.abs(time);
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
int result = -1; int result = -1;
if (timeDiff <= hitResultOffset[GameData.HIT_300]) if (timeDiff <= hitResultOffset[GameData.HIT_300])
@ -142,13 +141,13 @@ public class Circle implements GameObject {
@Override @Override
public boolean mousePressed(int x, int y, int trackPosition) { public boolean mousePressed(int x, int y, int trackPosition) {
double distance = Math.hypot(this.x - x, this.y - y); double distance = Math.hypot(this.x - x, this.y - y);
if (distance < diameter/2) { if (distance < diameter / 2) {
int timeDiff = trackPosition - hitObject.getTime(); int timeDiff = trackPosition - hitObject.getTime();
int result = hitResult(timeDiff); int result = hitResult(timeDiff);
if (result > -1) { if (result > -1) {
data.addHitError(hitObject.getTime(), x, y, timeDiff); 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; return true;
} }
} }
@ -164,17 +163,17 @@ public class Circle implements GameObject {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) // "auto" mod: catch any missed notes due to lag 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 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; return true;
} }
// "auto" mod: send a perfect hit result // "auto" mod: send a perfect hit result
else if (isAutoMod) { else if (isAutoMod) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) { 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; return true;
} }
} }
@ -199,7 +198,5 @@ public class Circle implements GameObject {
} }
@Override @Override
public void reset() { public void reset() {}
}
} }

View File

@ -65,7 +65,5 @@ public class DummyObject implements GameObject {
} }
@Override @Override
public void reset() { public void reset() {}
}
} }

View File

@ -71,9 +71,7 @@ public interface GameObject {
public void updatePosition(); 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(); public void reset();
} }

View File

@ -50,8 +50,10 @@ public class Slider implements GameObject {
/** Rate at which slider ticks are placed. */ /** Rate at which slider ticks are placed. */
private static float sliderTickRate = 1.0f; private static float sliderTickRate = 1.0f;
/** Follow circle radius. */
private static float followRadius; private static float followRadius;
/** The diameter of hit circles. */
private static float diameter; private static float diameter;
/** The amount of time, in milliseconds, to fade in the slider. */ /** 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. */ /** The time duration of the slider including repeats, in milliseconds. */
private float sliderTimeTotal = 0f; private float sliderTimeTotal = 0f;
/** Whether or not the result of the initial/final hit circles have been processed. */ /** Whether or not the result of the initial hit circle has been processed. */
private boolean sliderClickedInitial = false, sliderClickedFinal = false; 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. */ /** Whether or not to show the follow circle. */
private boolean followCircleActive = false; private boolean followCircleActive = false;
@ -105,8 +110,6 @@ public class Slider implements GameObject {
/** Container dimensions. */ /** Container dimensions. */
private static int containerWidth, containerHeight; private static int containerWidth, containerHeight;
/** /**
* Initializes the Slider data type with images and dimensions. * Initializes the Slider data type with images and dimensions.
* @param container the game container * @param container the game container
@ -119,9 +122,10 @@ public class Slider implements GameObject {
diameter = (104 - (circleSize * 8)); diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter; int diameterInt = (int) diameter;
followRadius = diameter / 2 * 3f; followRadius = diameter / 2 * 3f;
// slider ball // slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() || if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
@ -326,8 +330,6 @@ public class Slider implements GameObject {
80 - 10*3 - 50 - 0 (3 tick only)(0x) 80 - 10*3 - 50 - 0 (3 tick only)(0x)
70 - 10*2 - 50 - 0 (2 tick only)(0x) 70 - 10*2 - 50 - 0 (2 tick only)(0x)
60 - 10 - 50 - 0 (1 tick only)(0x) 60 - 10 - 50 - 0 (1 tick only)(0x)
*/ */
float tickRatio = (float) ticksHit / tickIntervals; float tickRatio = (float) ticksHit / tickIntervals;
@ -354,7 +356,8 @@ public class Slider implements GameObject {
type = HitObjectType.SLIDER_FIRST; type = HitObjectType.SLIDER_FIRST;
} }
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, 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; return result;
} }
@ -429,21 +432,17 @@ public class Slider implements GameObject {
float[] c = curve.pointAt(getT(trackPosition, false)); float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY); double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
if (distance < followRadius) if (distance < followRadius)
sliderClickedFinal = true; sliderHeldToEnd = true;
} }
// final circle hit // final circle hit
if (sliderClickedFinal){ if (sliderHeldToEnd)
ticksHit++; ticksHit++;
data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats);
}
// "auto" mod: always send a perfect hit result // "auto" mod: always send a perfect hit result
if (isAutoMod) if (isAutoMod)
ticksHit = tickIntervals; ticksHit = tickIntervals;
//TODO missing the final shouldn't increment the combo
// calculate and send slider result // calculate and send slider result
hitResult(); hitResult();
return true; return true;
@ -501,8 +500,8 @@ public class Slider implements GameObject {
} }
// held near end of slider // held near end of slider
if (!sliderClickedFinal && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300]) if (!sliderHeldToEnd && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300])
sliderClickedFinal = true; sliderHeldToEnd = true;
} else { } else {
followCircleActive = false; followCircleActive = false;
@ -563,12 +562,11 @@ public class Slider implements GameObject {
@Override @Override
public void reset() { public void reset() {
sliderClickedInitial = false; sliderClickedInitial = false;
sliderClickedFinal = false; sliderHeldToEnd = false;
followCircleActive = false; followCircleActive = false;
currentRepeats = 0; currentRepeats = 0;
tickIndex = 0; tickIndex = 0;
ticksHit = 0; ticksHit = 0;
tickIntervals = 1; tickIntervals = 1;
} }
} }

View File

@ -58,13 +58,14 @@ public class Spinner implements GameObject {
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI; AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
SPUN_OUT_MULTIPLIER = 1 / 33.25f; // angle = 287/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. */ /** PI constants. */
private static final float private static final float
TWO_PI = (float) (Math.PI * 2), TWO_PI = (float) (Math.PI * 2),
HALF_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. */ /** The associated HitObject. */
private HitObject hitObject; private HitObject hitObject;
@ -124,6 +125,7 @@ public class Spinner implements GameObject {
public Spinner(HitObject hitObject, Game game, GameData data) { public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject; this.hitObject = hitObject;
this.data = data; this.data = data;
/* /*
1 beat = 731.707317073171ms 1 beat = 731.707317073171ms
RPM at frame X with spinner Y beats long RPM at frame X with spinner Y beats long
@ -152,16 +154,16 @@ public class Spinner implements GameObject {
25.00 53 154 272 391 447 466 25.00 53 154 272 391 447 466
^beats ^beats
*/ */
//TODO not correct at all, but close enough? // TODO not correct at all, but close enough?
//<2sec ~ 12 ~ 200ms // <2sec ~ 12 ~ 200ms
//>5sec ~ 48 ~ 800ms // >5sec ~ 48 ~ 800ms
final int minVel = 12; final int minVel = 12;
final int maxVel = 48; final int maxVel = 48;
final int minTime = 2000; final int minTime = 2000;
final int maxTime = 5000; final int maxTime = 5000;
maxStoredDeltaAngles = (int) Utils.clamp( maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
(hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel-minVel)/(maxTime-minTime) + minVel * (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
, minVel, maxVel);
storedDeltaAngle = new float[maxStoredDeltaAngles]; storedDeltaAngle = new float[maxStoredDeltaAngles];
// calculate rotations needed // calculate rotations needed
@ -245,7 +247,7 @@ public class Spinner implements GameObject {
result = GameData.HIT_MISS; result = GameData.HIT_MISS;
data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2, 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; return result;
} }
@ -257,7 +259,6 @@ public class Spinner implements GameObject {
@Override @Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) { public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
// end of spinner // end of spinner
if (overlap || trackPosition > hitObject.getEndTime()) { if (overlap || trackPosition > hitObject.getEndTime()) {
hitResult(); hitResult();
@ -289,13 +290,12 @@ public class Spinner implements GameObject {
isSpinning = true; isSpinning = true;
return false; return false;
} }
angleDiff = angle - lastAngle;
if(Math.abs(angleDiff) > 0.01f){
lastAngle = angle;
}else{
angleDiff = 0;
}
angleDiff = angle - lastAngle;
if (Math.abs(angleDiff) > 0.01f)
lastAngle = angle;
else
angleDiff = 0;
} }
// make angleDiff the smallest angle change possible // make angleDiff the smallest angle change possible
@ -305,14 +305,14 @@ public class Spinner implements GameObject {
else if (angleDiff > Math.PI) else if (angleDiff > Math.PI)
angleDiff -= TWO_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) if (isSpinning)
deltaAngleOverflow += angleDiff; deltaAngleOverflow += angleDiff;
while (deltaOverflow >= DELTA_UPDATE_TIME) { while (deltaOverflow >= DELTA_UPDATE_TIME) {
// spin caused by the cursor // spin caused by the cursor
float deltaAngle = 0; float deltaAngle = 0;
if (isSpinning){ if (isSpinning) {
deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow; deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
deltaAngleOverflow -= deltaAngle; deltaAngleOverflow -= deltaAngle;
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
@ -325,15 +325,15 @@ public class Spinner implements GameObject {
float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles; float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles;
rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF); rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI; float rotationPerSec = rotationAngle * (1000 / DELTA_UPDATE_TIME) / TWO_PI;
drawnRPM = (int)(Math.abs(rotationPerSec * 60)); drawnRPM = (int) (Math.abs(rotationPerSec * 60));
rotate(rotationAngle); rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f) if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER); data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
} }
//TODO may need to update 1 more time when the spinner ends? //TODO may need to update 1 more time when the spinner ends?
return false; 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 //TODO seems to give 1100 points per spin but also an extra 100 for some spinners
if (newRotations > rotationsNeeded) { // extra rotations if (newRotations > rotationsNeeded) { // extra rotations
data.changeScore(1000); data.changeScore(1000);
SoundController.playSound(SoundEffect.SPINNERBONUS); SoundController.playSound(SoundEffect.SPINNERBONUS);
} }
data.changeScore(100); data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN); SoundController.playSound(SoundEffect.SPINNERSPIN);
}
} // extra 100 for some spinners (mostly wrong)
/* // if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) {
//The extra 100 for some spinners (mostly wrong) // if (newRotations + 0.5f > rotationsNeeded) // extra rotations
if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) { // data.changeScore(100);
if (newRotations + 0.5f > rotationsNeeded) { // extra rotations // }
data.changeScore(100);
}
}
//*/
rotations = newRotations; rotations = newRotations;
} }
@ -403,9 +399,8 @@ public class Spinner implements GameObject {
public void reset() { public void reset() {
deltaAngleIndex = 0; deltaAngleIndex = 0;
sumDeltaAngle = 0; sumDeltaAngle = 0;
for(int i=0; i<storedDeltaAngle.length; i++){ for (int i = 0; i < storedDeltaAngle.length; i++)
storedDeltaAngle[i] = 0; storedDeltaAngle[i] = 0;
}
drawRotation = 0; drawRotation = 0;
rotations = 0; rotations = 0;
deltaOverflow = 0; deltaOverflow = 0;

View File

@ -18,6 +18,7 @@
package itdelatrisu.opsu.render; package itdelatrisu.opsu.render;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GL30;

View File

@ -56,6 +56,9 @@ public class Replay {
/** The associated file. */ /** The associated file. */
private File file; private File file;
/** The associated score data. */
private ScoreData scoreData;
/** Whether or not the replay data has been loaded from the file. */ /** Whether or not the replay data has been loaded from the file. */
public boolean loaded = false; public boolean loaded = false;
@ -104,8 +107,6 @@ public class Replay {
/** Seed. (?) */ /** Seed. (?) */
public int seed; public int seed;
private ScoreData scoreData;
/** Seed string. */ /** Seed string. */
private static final String SEED_STRING = "-12345"; private static final String SEED_STRING = "-12345";
@ -137,45 +138,15 @@ public class Replay {
loaded = true; loaded = true;
} }
/**
* Loads the replay header only.
* @throws IOException failure to load the data
*/
public void loadHeader() throws IOException { public void loadHeader() throws IOException {
OsuReader reader = new OsuReader(file); OsuReader reader = new OsuReader(file);
loadHeader(reader); loadHeader(reader);
reader.close(); 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. * 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. * Saves the replay data to a file in the replays directory.
*/ */

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.replay; package itdelatrisu.opsu.replay;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.db.ScoreDB;
import java.io.File; import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import org.newdawn.slick.util.Log; /**
* Importer for replay files.
*/
public class ReplayImporter { 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) { public static void importAllReplaysFromDir(File dir) {
for (File replayToImport : dir.listFiles()) { // find all OSR files
try { files = dir.listFiles(new FilenameFilter() {
Replay r = new Replay(replayToImport); @Override
r.loadHeader(); public boolean accept(File dir, String name) {
Beatmap oFile = BeatmapSetList.get().getFileFromBeatmapHash(r.beatmapHash); return name.toLowerCase().endsWith(".osr");
if(oFile != null){ }
File replaydir = Options.getReplayDir(); });
if (!replaydir.isDirectory()) { if (files == null || files.length < 1) {
if (!replaydir.mkdir()) { files = null;
ErrorHandler.error("Failed to create replay directory.", null, false); 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; return;
} }
} }
//ErrorHandler.error("Importing"+replayToImport+" forBeatmap:"+oFile, null, false);
ScoreData data = r.getScoreData(oFile); // import OSRs
File moveToFile = new File(replaydir, replayToImport.getName()); for (File file : files) {
if( fileIndex++;
!replayToImport.renameTo(moveToFile) Replay r = new Replay(file);
){ try {
Log.warn("Rename Failed "+moveToFile); r.loadHeader();
}
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) { } catch (IOException e) {
Log.warn("Failed to import replays ",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;
} }
} }

View File

@ -26,9 +26,9 @@ import itdelatrisu.opsu.Utils;
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.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode; import itdelatrisu.opsu.downloads.DownloadNode;

View File

@ -217,18 +217,15 @@ public class Game extends BasicGameState {
/** Playback speed (used in replays and "auto" mod). */ /** Playback speed (used in replays and "auto" mod). */
private PlaybackSpeed playbackSpeed; private PlaybackSpeed playbackSpeed;
/** Whether the game is currently seeking to a replay position. */
private boolean isSeeking;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
private Input input; private Input input;
private int state; private int state;
private int width;
private int height;
private boolean seeking;
public Game(int state) { public Game(int state) {
this.state = state; this.state = state;
} }
@ -240,8 +237,8 @@ public class Game extends BasicGameState {
this.game = game; this.game = game;
input = container.getInput(); input = container.getInput();
width = container.getWidth(); int width = container.getWidth();
height = container.getHeight(); int height = container.getHeight();
// create offscreen graphics // create offscreen graphics
offscreen = new Image(width, height); offscreen = new Image(width, height);
@ -617,13 +614,14 @@ public class Game extends BasicGameState {
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed); updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
//TODO probably should to disable sounds then reseek to the new position //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; replayIndex = 0;
while(objectIndex>=0){ while (objectIndex >= 0) {
gameObjects[objectIndex].reset(); gameObjects[objectIndex].reset();
objectIndex--; objectIndex--;
} }
// reset game data // reset game data
resetGameData(); resetGameData();
@ -635,7 +633,7 @@ public class Game extends BasicGameState {
timingPointIndex++; timingPointIndex++;
} }
} }
seeking = false; isSeeking = false;
} }
// update and run replay frames // update and run replay frames
@ -925,10 +923,11 @@ public class Game extends BasicGameState {
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier()); MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
} }
if(!GameMod.AUTO.isActive() && y < 50){ // TODO
float pos = (float)x / width * beatmap.endTime; else if (!GameMod.AUTO.isActive() && y < 50) {
MusicController.setPosition((int)pos); float pos = (float) x / container.getWidth() * beatmap.endTime;
seeking = true; MusicController.setPosition((int) pos);
isSeeking = true;
} }
return; return;
} }
@ -1439,19 +1438,16 @@ public class Game extends BasicGameState {
// overallDifficulty (hit result time offsets) // overallDifficulty (hit result time offsets)
hitResultOffset = new int[GameData.HIT_MAX]; 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_300] = (int) (78 - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8)); hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10)); hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (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); data.setHitResultOffset(hitResultOffset);
//*/
// HPDrainRate (health change) // HPDrainRate (health change)
data.setDrainRate(HPDrainRate); data.setDrainRate(HPDrainRate);

View File

@ -32,8 +32,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.MenuButton.Expand; import itdelatrisu.opsu.ui.MenuButton.Expand;
import itdelatrisu.opsu.ui.UI;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.IOException; import java.io.IOException;

View File

@ -32,10 +32,10 @@ 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.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder; import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;

View File

@ -25,9 +25,9 @@ import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;

View File

@ -25,6 +25,7 @@ import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.replay.ReplayImporter;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.UIManager; import javax.swing.UIManager;
@ -285,6 +286,9 @@ public class UI {
text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ? text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ?
"Updating database..." : "Loading beatmaps..."; "Updating database..." : "Loading beatmaps...";
progress = BeatmapParser.getParserProgress(); progress = BeatmapParser.getParserProgress();
} else if ((file = ReplayImporter.getCurrentFileName()) != null) {
text = "Importing replays...";
progress = ReplayImporter.getLoadingProgress();
} else if ((file = SoundController.getCurrentFileName()) != null) { } else if ((file = SoundController.getCurrentFileName()) != null) {
text = "Loading sounds..."; text = "Loading sounds...";
progress = SoundController.getLoadingProgress(); progress = SoundController.getLoadingProgress();

View File

@ -41,7 +41,6 @@ import org.lwjgl.openal.OpenALException;
import org.newdawn.slick.util.Log; import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader; import org.newdawn.slick.util.ResourceLoader;
/** /**
* A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL * A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL
* as required. * as required.
@ -356,7 +355,7 @@ public class OpenALStreamPlayer {
} }
playedPos = streamPos; playedPos = streamPos;
syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate)/pitch); syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate) / pitch);
startPlayback(); startPlayback();
@ -409,7 +408,6 @@ public class OpenALStreamPlayer {
// hard reset // hard reset
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) { if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
//System.out.println("Time HARD Reset"+" "+thisPosition+" "+(dxTime / 1000f));
syncPosition(); syncPosition();
dxTime = (thisTime - syncStartTime) * pitch; dxTime = (thisTime - syncStartTime) * pitch;
avgDiff = 0; avgDiff = 0;
@ -418,19 +416,22 @@ public class OpenALStreamPlayer {
float diff = thisPosition * 1000 - (dxTime); float diff = thisPosition * 1000 - (dxTime);
avgDiff = (diff + avgDiff * 9) / 10; avgDiff = (diff + avgDiff * 9) / 10;
if(Math.abs(avgDiff) >= 1){ if (Math.abs(avgDiff) >= 1) {
syncStartTime -= (int)(avgDiff); syncStartTime -= (int) (avgDiff);
avgDiff -= (int)(avgDiff); avgDiff -= (int) (avgDiff);
dxTime = (thisTime - syncStartTime) * pitch; dxTime = (thisTime - syncStartTime) * pitch;
} }
lastUpdatePosition = thisPosition; lastUpdatePosition = thisPosition;
} }
return dxTime / 1000f; 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; avgDiff = 0;
} }