Merge pull request #99 from fluddokt/ReplayTest

Replay importing, spinner fixes (fixes #67), replay seeking, in-place MD5 calculation, pitch change time sync (fixes #86).
This commit is contained in:
Jeffrey Han 2015-06-28 21:14:10 -05:00
commit 7d08a7d391
22 changed files with 622 additions and 96 deletions

View File

@ -1207,8 +1207,13 @@ 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
@ -1219,19 +1224,26 @@ 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
* @return the score value * @return the score value
*/ */
private int getScoreForHit(int hitValue) { private int getScoreForHit(int hitValue, HitObject hitObject) {
return hitValue + (int) (hitValue * (Math.max(combo - 1, 0) * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25); int comboMulti = Math.max(combo - 1, 0);
if(hitObject.isSlider()){
comboMulti += 1;
}
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
*/ */
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) { public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) {
//TODO THE LIES ( difficultyMultiplier )
//*
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;
@ -1243,8 +1255,21 @@ public class GameData {
difficultyMultiplier = 5; difficultyMultiplier = 5;
else //if (sum <= 30f) else //if (sum <= 30f)
difficultyMultiplier = 6; 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;
*/
}
/** /**
* 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
@ -1292,7 +1317,7 @@ public class GameData {
hitObject.getAdditionSampleSet(repeat)); hitObject.getAdditionSampleSet(repeat));
// calculate score and increment combo streak // calculate score and increment combo streak
changeScore(getScoreForHit(hitValue)); changeScore(getScoreForHit(hitValue, hitObject));
incrementComboStreak(); incrementComboStreak();
} }
hitResultCount[result]++; hitResultCount[result]++;
@ -1357,6 +1382,7 @@ 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
@ -1387,6 +1413,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?
return scoreData; return scoreData;
} }
@ -1407,7 +1434,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) ? "" : Utils.getMD5(beatmap.getFile()); replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash;//Utils.getMD5(beatmap.getFile());
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

@ -0,0 +1,91 @@
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

@ -110,6 +110,9 @@ public class Options {
/** The replay directory (created when needed). */ /** The replay directory (created when needed). */
private static File replayDir; private static File replayDir;
/** The replay import directory. */
private static File replayImportDir;
/** The root skin directory. */ /** The root skin directory. */
private static File skinRootDir; private static File skinRootDir;
@ -1088,6 +1091,20 @@ public class Options {
return oszDir; return oszDir;
} }
/**
* Returns the replay import directory.
* If invalid, this will create and return a "ReplayImport" directory.
* @return the replay import directory
*/
public static File getReplayImportDir() {
if (replayImportDir != null && replayImportDir.isDirectory())
return replayImportDir;
replayImportDir = new File(DATA_DIR, "ReplayImport/");
replayImportDir.mkdir();
return replayImportDir;
}
/** /**
* Returns the screenshot directory. * Returns the screenshot directory.
* If invalid, this will return a "Screenshot" directory. * If invalid, this will return a "Screenshot" directory.

View File

@ -78,6 +78,9 @@ 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;
@ -164,6 +167,7 @@ public class ScoreData implements Comparable<ScoreData> {
this.perfect = rs.getBoolean(16); this.perfect = rs.getBoolean(16);
this.mods = rs.getInt(17); this.mods = rs.getInt(17);
this.replayString = rs.getString(18); this.replayString = rs.getString(18);
this.playerName = rs.getString(19);
} }
/** /**
@ -260,7 +264,7 @@ public class ScoreData implements Comparable<ScoreData> {
// hit counts (custom: osu! shows user instead, above score) // hit counts (custom: osu! shows user instead, above score)
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", hit300, hit100, hit50, miss), String.format("300:%d 100:%d 50:%d Miss:%d Name:%s", hit300, hit100, hit50, miss, getPlayerName()),
Color.white Color.white
); );
@ -331,6 +335,11 @@ 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,6 +185,9 @@ 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 */
public String md5Hash;
/** /**
* [HitObjects] * [HitObjects]
*/ */

View File

@ -19,16 +19,20 @@
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 java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -205,7 +209,15 @@ public class BeatmapParser {
Beatmap beatmap = new Beatmap(file); Beatmap beatmap = new Beatmap(file);
beatmap.timingPoints = new ArrayList<TimingPoint>(); beatmap.timingPoints = new ArrayList<TimingPoint>();
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) { 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"));
String line = in.readLine(); String line = in.readLine();
String tokens[] = null; String tokens[] = null;
while (line != null) { while (line != null) {
@ -578,6 +590,8 @@ public class BeatmapParser {
break; break;
} }
} }
if (md5stream != null)
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);
} }

View File

@ -28,6 +28,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
@ -58,6 +59,10 @@ 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 . */
public HashMap<String, Beatmap> beatmapHashesToFile;
/** 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;
@ -83,6 +88,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>();
reset(); reset();
} }
@ -117,6 +123,10 @@ 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);
}
return node; return node;
} }
@ -501,4 +511,8 @@ public class BeatmapSetList {
* @return true if id is in the list * @return true if id is in the list
*/ */
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);
}
} }

View File

@ -43,7 +43,7 @@ public class BeatmapDB {
* Current database version. * Current database version.
* This value should be changed whenever the database format changes. * This value should be changed whenever the database format changes.
*/ */
private static final String DATABASE_VERSION = "2014-06-08"; private static final String DATABASE_VERSION = "2015-06-11";
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */ /** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f; private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@ -96,7 +96,7 @@ public class BeatmapDB {
insertStmt = connection.prepareStatement( insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" + "INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
); );
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?"); selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?"); deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
@ -122,7 +122,8 @@ 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" +
"); " + "); " +
"CREATE TABLE IF NOT EXISTS info (" + "CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" + "key TEXT NOT NULL UNIQUE, value TEXT" +
@ -340,6 +341,7 @@ public class BeatmapDB {
stmt.setString(38, beatmap.timingPointsToString()); stmt.setString(38, beatmap.timingPointsToString());
stmt.setString(39, beatmap.breaksToString()); stmt.setString(39, beatmap.breaksToString());
stmt.setString(40, beatmap.comboToString()); stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash);
} catch (SQLException e) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@ -481,6 +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));
} catch (SQLException e) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {

View File

@ -45,7 +45,7 @@ public class ScoreDB {
* This value should be changed whenever the database format changes. * This value should be changed whenever the database format changes.
* Add any update queries to the {@link #getUpdateQueries(int)} method. * Add any update queries to the {@link #getUpdateQueries(int)} method.
*/ */
private static final int DATABASE_VERSION = 20140311; private static final int DATABASE_VERSION = 20150401;
/** /**
* Returns a list of SQL queries to apply, in order, to update from * Returns a list of SQL queries to apply, in order, to update from
@ -57,6 +57,8 @@ public class ScoreDB {
List<String> list = new LinkedList<String>(); List<String> list = new LinkedList<String>();
if (version < 20140311) if (version < 20140311)
list.add("ALTER TABLE scores ADD COLUMN replay TEXT"); list.add("ALTER TABLE scores ADD COLUMN replay TEXT");
if (version < 20150401)
list.add("ALTER TABLE scores ADD COLUMN playerName TEXT");
/* add future updates here */ /* add future updates here */
@ -95,8 +97,13 @@ 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(
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
); );
selectMapStmt = connection.prepareStatement( selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " + "SELECT * FROM scores WHERE " +
@ -114,7 +121,8 @@ public class ScoreDB {
"DELETE FROM scores WHERE " + "DELETE FROM scores WHERE " +
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " + "timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " + "creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ?" "geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"replay = ? AND playerName = ?"
); );
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Failed to prepare score statements.", e, true); ErrorHandler.error("Failed to prepare score statements.", e, true);
@ -137,7 +145,8 @@ public class ScoreDB {
"combo INTEGER, " + "combo INTEGER, " +
"perfect BOOLEAN, " + "perfect BOOLEAN, " +
"mods INTEGER, " + "mods INTEGER, " +
"replay TEXT" + "replay TEXT," +
"playerName TEXT"+
");" + ");" +
"CREATE TABLE IF NOT EXISTS info (" + "CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" + "key TEXT NOT NULL UNIQUE, value TEXT" +
@ -283,6 +292,9 @@ public class ScoreDB {
stmt.setInt(15, data.combo); stmt.setInt(15, data.combo);
stmt.setBoolean(16, data.perfect); stmt.setBoolean(16, data.perfect);
stmt.setInt(17, data.mods); stmt.setInt(17, data.mods);
stmt.setString(18, data.replayString);
stmt.setString(19, data.playerName);
} }
/** /**

View File

@ -18,6 +18,7 @@
package itdelatrisu.opsu.io; package itdelatrisu.opsu.io;
import java.io.BufferedInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -50,7 +51,7 @@ public class OsuReader {
* @param source the input stream to read from * @param source the input stream to read from
*/ */
public OsuReader(InputStream source) { public OsuReader(InputStream source) {
this.reader = new DataInputStream(source); this.reader = new DataInputStream(new BufferedInputStream(source));
} }
/** /**

View File

@ -18,6 +18,7 @@
package itdelatrisu.opsu.io; package itdelatrisu.opsu.io;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -51,7 +52,7 @@ public class OsuWriter {
* @param dest the output stream to write to * @param dest the output stream to write to
*/ */
public OsuWriter(OutputStream dest) { public OsuWriter(OutputStream dest) {
this.writer = new DataOutputStream(dest); this.writer = new DataOutputStream(new BufferedOutputStream(dest));
} }
/** /**

View File

@ -38,6 +38,9 @@ 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 */
private static float diameter;
/** The associated HitObject. */ /** The associated HitObject. */
private HitObject hitObject; private HitObject hitObject;
@ -62,11 +65,12 @@ public class Circle implements GameObject {
* @param circleSize the map's circleSize value * @param circleSize the map's circleSize value
*/ */
public static void init(GameContainer container, float circleSize) { public static void init(GameContainer container, float circleSize) {
int diameter = (int) (104 - (circleSize * 8)); diameter = (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter)); int diameterInt = (int)diameter;
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter)); GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter)); GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
} }
/** /**
@ -119,15 +123,16 @@ 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])
result = GameData.HIT_300; result = GameData.HIT_300;
else if (timeDiff < hitResultOffset[GameData.HIT_100]) else if (timeDiff <= hitResultOffset[GameData.HIT_100])
result = GameData.HIT_100; result = GameData.HIT_100;
else if (timeDiff < hitResultOffset[GameData.HIT_50]) else if (timeDiff <= hitResultOffset[GameData.HIT_50])
result = GameData.HIT_50; result = GameData.HIT_50;
else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) else if (timeDiff <= hitResultOffset[GameData.HIT_MISS])
result = GameData.HIT_MISS; result = GameData.HIT_MISS;
//else not a hit //else not a hit
@ -137,8 +142,7 @@ 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);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; if (distance < diameter/2) {
if (distance < circleRadius) {
int timeDiff = trackPosition - hitObject.getTime(); int timeDiff = trackPosition - hitObject.getTime();
int result = hitResult(timeDiff); int result = hitResult(timeDiff);
@ -158,7 +162,7 @@ public class Circle implements GameObject {
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive(); boolean isAutoMod = GameMod.AUTO.isActive();
if (overlap || 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, 0, HitObjectType.CIRCLE, null, true);
@ -193,4 +197,9 @@ public class Circle implements GameObject {
this.x = hitObject.getScaledX(); this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY(); this.y = hitObject.getScaledY();
} }
@Override
public void reset() {
}
} }

View File

@ -63,4 +63,9 @@ public class DummyObject implements GameObject {
this.x = hitObject.getScaledX(); this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY(); this.y = hitObject.getScaledY();
} }
@Override
public void reset() {
}
} }

View File

@ -69,4 +69,11 @@ public interface GameObject {
* Updates the position of the hit object. * Updates the position of the hit object.
*/ */
public void updatePosition(); public void updatePosition();
/**
* Resets the hit object so that it can be reused.
*/
public void reset();
} }

View File

@ -50,6 +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;
private static float followRadius;
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. */
private static final int FADE_IN_TIME = 375; private static final int FADE_IN_TIME = 375;
@ -101,6 +105,8 @@ 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
@ -111,9 +117,11 @@ public class Slider implements GameObject {
containerWidth = container.getWidth(); containerWidth = container.getWidth();
containerHeight = container.getHeight(); containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8)); diameter = (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter;
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))
@ -121,11 +129,11 @@ public class Slider implements GameObject {
else else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() }; sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
for (int i = 0; i < sliderBallImages.length; i++) for (int i = 0; i < sliderBallImages.length; i++)
sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128); sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameterInt * 118 / 128, diameterInt * 118 / 128);
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128)); GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameterInt * 259 / 128, diameterInt * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter)); GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameter / 4, diameter / 4)); GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4));
sliderMultiplier = beatmap.sliderMultiplier; sliderMultiplier = beatmap.sliderMultiplier;
sliderTickRate = beatmap.sliderTickRate; sliderTickRate = beatmap.sliderTickRate;
@ -272,6 +280,55 @@ public class Slider implements GameObject {
* @return the hit result (GameData.HIT_* constants) * @return the hit result (GameData.HIT_* constants)
*/ */
private int hitResult() { private int hitResult() {
/*
time scoredelta score-hit-initial-tick= unaccounted
(1/4 - 1) 396 - 300 - 30 46
(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
difficultyMulti = 3 (+36 per combo)
score =
(t)ticks(10) * nticks +
(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; float tickRatio = (float) ticksHit / tickIntervals;
int result; int result;
@ -308,8 +365,7 @@ public class Slider implements GameObject {
return false; return false;
double distance = Math.hypot(this.x - x, this.y - y); double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; if (distance < diameter / 2) {
if (distance < circleRadius) {
int timeDiff = Math.abs(trackPosition - hitObject.getTime()); int timeDiff = Math.abs(trackPosition - hitObject.getTime());
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
@ -365,26 +421,29 @@ public class Slider implements GameObject {
} }
// end of slider // end of slider
if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) { if (trackPosition > hitObject.getTime() + sliderTimeTotal) {
tickIntervals++; tickIntervals++;
// check if cursor pressed and within end circle // check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) { if (keyPressed || GameMod.RELAX.isActive()) {
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);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; if (distance < followRadius)
if (distance < followCircleRadius)
sliderClickedFinal = true; sliderClickedFinal = true;
} }
// final circle hit // final circle hit
if (sliderClickedFinal) if (sliderClickedFinal){
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;
@ -417,8 +476,7 @@ public class Slider implements GameObject {
// holding slider... // holding slider...
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);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2; if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) {
// mouse pressed and within follow circle // mouse pressed and within follow circle
followCircleActive = true; followCircleActive = true;
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER); data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
@ -501,4 +559,16 @@ public class Slider implements GameObject {
return (floor % 2 == 0) ? t - floor : floor + 1 - t; return (floor % 2 == 0) ? t - floor : floor + 1 - t;
} }
} }
@Override
public void reset() {
sliderClickedInitial = false;
sliderClickedFinal = false;
followCircleActive = false;
currentRepeats = 0;
tickIndex = 0;
ticksHit = 0;
tickIntervals = 1;
}
} }

View File

@ -45,11 +45,10 @@ public class Spinner implements GameObject {
private static float overallDifficulty = 5f; private static float overallDifficulty = 5f;
/** The number of rotation velocities to store. */ /** The number of rotation velocities to store. */
// note: currently takes about 200ms to spin up (4 * 50) private int maxStoredDeltaAngles;
private static final int MAX_ROTATION_VELOCITIES = 50;
/** The amount of time, in milliseconds, before another velocity is stored. */ /** The amount of time, in milliseconds, before another velocity is stored. */
private static final int DELTA_UPDATE_TIME = 4; private static final float DELTA_UPDATE_TIME = 1000 / 60f;
/** The amount of time, in milliseconds, to fade in the spinner. */ /** The amount of time, in milliseconds, to fade in the spinner. */
private static final int FADE_IN_TIME = 500; private static final int FADE_IN_TIME = 500;
@ -64,6 +63,8 @@ public class Spinner implements GameObject {
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;
@ -83,19 +84,25 @@ public class Spinner implements GameObject {
private float rotationsNeeded; private float rotationsNeeded;
/** The remaining amount of time that was not used. */ /** The remaining amount of time that was not used. */
private int deltaOverflow; private float deltaOverflow;
/** The sum of all the velocities in storedVelocities. */ /** The sum of all the velocities in storedVelocities. */
private float sumVelocity = 0f; private float sumDeltaAngle = 0f;
/** Array holding the most recent rotation velocities. */ /** Array holding the most recent rotation velocities. */
private float[] storedVelocities = new float[MAX_ROTATION_VELOCITIES]; private float[] storedDeltaAngle;
/** True if the mouse cursor is pressed. */ /** True if the mouse cursor is pressed. */
private boolean isSpinning; private boolean isSpinning;
/** Current index of the stored velocities in rotations/second. */ /** Current index of the stored velocities in rotations/second. */
private int velocityIndex = 0; 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;
/** /**
* Initializes the Spinner data type with images and dimensions. * Initializes the Spinner data type with images and dimensions.
@ -117,6 +124,45 @@ 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
RPM at frame X with spinner Y beats long
10 20 30 40 50 60 <frame#
1.00 306 418 457 470
1.25 323 424 459 471 475
1.5 305 417 456 470 475 477
1.75 322 417 456 471 475
2.00 304 410 454 469 474 476
2.25 303 410 451 467 474 476
2.50 303 417 456 470 475 476
2.75 302 416 456 470 475 476
3.00 301 416 456 470 475 <-- ~2sec
4.00 274 414 453 470 475
5.00 281 409 454 469 475
6.00 232 392 451 467 472 476
6.25 193 378 443 465
6.50 133 344 431 461
6.75 85 228 378 435 463 472 <-- ~5sec
7.00 53 154 272 391 447
8.00 53 154 272 391 447
9.00 53 154 272 400 450
10.00 53 154 272 400 450
15.00 53 154 272 391 444 466
20.00 61 154 272 400 447
25.00 53 154 272 391 447 466
^beats
*/
//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);
storedDeltaAngle = new float[maxStoredDeltaAngles];
// calculate rotations needed // calculate rotations needed
float spinsPerMinute = 100 + (overallDifficulty * 15); float spinsPerMinute = 100 + (overallDifficulty * 15);
@ -144,12 +190,11 @@ public class Spinner implements GameObject {
} }
// rpm // rpm
int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60));
Image rpmImg = GameImage.SPINNER_RPM.getImage(); Image rpmImg = GameImage.SPINNER_RPM.getImage();
rpmImg.setAlpha(alpha); rpmImg.setAlpha(alpha);
rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f); rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f);
if (timeDiff < 0) if (timeDiff < 0)
data.drawSymbolString(Integer.toString(rpm), (width + rpmImg.getWidth() * 0.95f) / 2f, data.drawSymbolString(Integer.toString(drawnRPM), (width + rpmImg.getWidth() * 0.95f) / 2f,
height - data.getScoreSymbolImage('0').getHeight() * 1.025f, 1f, 1f, true); height - data.getScoreSymbolImage('0').getHeight() * 1.025f, 1f, 1f, true);
// spinner meter (subimage) // spinner meter (subimage)
@ -205,7 +250,10 @@ public class Spinner implements GameObject {
} }
@Override @Override
public boolean mousePressed(int x, int y, int trackPosition) { return false; } // not used public boolean mousePressed(int x, int y, int trackPosition) {
lastAngle = (float) Math.atan2(x - (height / 2), y - (width / 2));
return false;
}
@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) {
@ -222,17 +270,18 @@ public class Spinner implements GameObject {
// spin automatically // spin automatically
// http://osu.ppy.sh/wiki/FAQ#Spinners // http://osu.ppy.sh/wiki/FAQ#Spinners
float angle;
deltaOverflow += delta;
float angleDiff = 0;
if (GameMod.AUTO.isActive()) { if (GameMod.AUTO.isActive()) {
lastAngle = 0; angleDiff = delta * AUTO_MULTIPLIER;
angle = delta * AUTO_MULTIPLIER;
isSpinning = true; isSpinning = true;
} else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) { } else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) {
lastAngle = 0; angleDiff = delta * SPUN_OUT_MULTIPLIER;
angle = delta * SPUN_OUT_MULTIPLIER;
isSpinning = true; isSpinning = true;
} else { } else {
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2)); float angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
// set initial angle to current mouse position to skip first click // set initial angle to current mouse position to skip first click
if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) { if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) {
@ -240,35 +289,52 @@ 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;
}
} }
// make angleDiff the smallest angle change possible // make angleDiff the smallest angle change possible
// (i.e. 1/4 rotation instead of 3/4 rotation) // (i.e. 1/4 rotation instead of 3/4 rotation)
float angleDiff = angle - lastAngle;
if (angleDiff < -Math.PI) if (angleDiff < -Math.PI)
angleDiff += TWO_PI; angleDiff += TWO_PI;
else if (angleDiff > Math.PI) else if (angleDiff > Math.PI)
angleDiff -= TWO_PI; angleDiff -= TWO_PI;
// spin caused by the cursor //may be a problem at higher frame rate due to float point round off
float cursorVelocity = 0;
if (isSpinning) if (isSpinning)
cursorVelocity = Utils.clamp(angleDiff / TWO_PI / delta * 1000, -8f, 8f); deltaAngleOverflow += angleDiff;
deltaOverflow += delta;
while (deltaOverflow >= DELTA_UPDATE_TIME) { while (deltaOverflow >= DELTA_UPDATE_TIME) {
sumVelocity -= storedVelocities[velocityIndex]; // spin caused by the cursor
sumVelocity += cursorVelocity; float deltaAngle = 0;
storedVelocities[velocityIndex++] = cursorVelocity; if (isSpinning){
velocityIndex %= storedVelocities.length; deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
deltaOverflow -= DELTA_UPDATE_TIME; deltaAngleOverflow -= deltaAngle;
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
} }
float rotationAngle = sumVelocity / storedVelocities.length * TWO_PI * delta / 1000; 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));
rotate(rotationAngle); rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f) if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER); data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
lastAngle = angle; }
//TODO may need to update 1 more time when the spinner ends?
return false; return false;
} }
@ -311,15 +377,38 @@ public class Spinner implements GameObject {
// added one whole rotation... // added one whole rotation...
if (Math.floor(newRotations) > rotations) { if (Math.floor(newRotations) > rotations) {
//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);
} else { }
data.changeScore(100); data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN); 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);
} }
} }
//*/
rotations = newRotations; rotations = newRotations;
} }
@Override
public void reset() {
deltaAngleIndex = 0;
sumDeltaAngle = 0;
for(int i=0; i<storedDeltaAngle.length; i++){
storedDeltaAngle[i] = 0;
}
drawRotation = 0;
rotations = 0;
deltaOverflow = 0;
isSpinning = false;
}
} }

View File

@ -20,14 +20,18 @@ 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.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.io.OsuReader; import itdelatrisu.opsu.io.OsuReader;
import itdelatrisu.opsu.io.OsuWriter; import itdelatrisu.opsu.io.OsuWriter;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder; import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -100,6 +104,8 @@ 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";
@ -131,6 +137,46 @@ public class Replay {
loaded = true; loaded = true;
} }
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. * Loads the replay header data.
* @param reader the associated reader * @param reader the associated reader
@ -232,7 +278,7 @@ public class Replay {
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try (FileOutputStream out = new FileOutputStream(file)) { try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
OsuWriter writer = new OsuWriter(out); OsuWriter writer = new OsuWriter(out);
// header // header

View File

@ -0,0 +1,49 @@
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.IOException;
import org.newdawn.slick.util.Log;
public class ReplayImporter {
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);
}
}
}
}

View File

@ -223,6 +223,12 @@ public class Game extends BasicGameState {
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;
} }
@ -234,8 +240,8 @@ public class Game extends BasicGameState {
this.game = game; this.game = game;
input = container.getInput(); input = container.getInput();
int width = container.getWidth(); width = container.getWidth();
int height = container.getHeight(); height = container.getHeight();
// create offscreen graphics // create offscreen graphics
offscreen = new Image(width, height); offscreen = new Image(width, height);
@ -610,6 +616,28 @@ public class Game extends BasicGameState {
if (replayIndex >= replay.frames.length) if (replayIndex >= replay.frames.length)
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
if(seeking && replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){
replayIndex = 0;
while(objectIndex>=0){
gameObjects[objectIndex].reset();
objectIndex--;
}
// reset game data
resetGameData();
// load the first timingPoint
if (!beatmap.timingPoints.isEmpty()) {
TimingPoint timingPoint = beatmap.timingPoints.get(0);
if (!timingPoint.isInherited()) {
setBeatLength(timingPoint, true);
timingPointIndex++;
}
}
seeking = false;
}
// update and run replay frames // update and run replay frames
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) { while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex]; ReplayFrame frame = replay.frames[replayIndex];
@ -753,7 +781,7 @@ public class Game extends BasicGameState {
while (objectIndex < gameObjects.length && trackPosition > beatmap.objects[objectIndex].getTime()) { while (objectIndex < gameObjects.length && trackPosition > beatmap.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time // check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < gameObjects.length && boolean overlap = (objectIndex + 1 < gameObjects.length &&
trackPosition > beatmap.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]); trackPosition > beatmap.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]);
// update hit object and check completion status // update hit object and check completion status
if (gameObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition)) if (gameObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
@ -897,6 +925,11 @@ public class Game extends BasicGameState {
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier()); 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;
}
return; return;
} }
@ -1044,6 +1077,9 @@ public class Game extends BasicGameState {
} else if (restart == Restart.REPLAY) } else if (restart == Restart.REPLAY)
retries = 0; retries = 0;
gameObjects = new GameObject[beatmap.objects.length];
playbackSpeed = PlaybackSpeed.NORMAL;
// reset game data // reset game data
resetGameData(); resetGameData();
@ -1063,7 +1099,7 @@ public class Game extends BasicGameState {
// is this the last note in the combo? // is this the last note in the combo?
boolean comboEnd = false; boolean comboEnd = false;
if (i + 1 < beatmap.objects.length && beatmap.objects[i + 1].isNewCombo()) if (i + 1 >= beatmap.objects.length || beatmap.objects[i + 1].isNewCombo())
comboEnd = true; comboEnd = true;
Color color = combo[hitObject.getComboIndex()]; Color color = combo[hitObject.getComboIndex()];
@ -1278,7 +1314,6 @@ public class Game extends BasicGameState {
* Resets all game data and structures. * Resets all game data and structures.
*/ */
public void resetGameData() { public void resetGameData() {
gameObjects = new GameObject[beatmap.objects.length];
data.clear(); data.clear();
objectIndex = 0; objectIndex = 0;
breakIndex = 0; breakIndex = 0;
@ -1303,7 +1338,6 @@ public class Game extends BasicGameState {
autoMouseY = 0; autoMouseY = 0;
autoMousePressed = false; autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3; flashlightRadius = container.getHeight() * 2 / 3;
playbackSpeed = PlaybackSpeed.NORMAL;
System.gc(); System.gc();
} }
@ -1405,11 +1439,19 @@ 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));
data.setHitResultOffset(hitResultOffset); data.setHitResultOffset(hitResultOffset);
//*/
// HPDrainRate (health change) // HPDrainRate (health change)
data.setDrainRate(HPDrainRate); data.setDrainRate(HPDrainRate);

View File

@ -25,6 +25,7 @@ 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.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.UI;
@ -128,6 +129,9 @@ public class Splash extends BasicGameState {
// parse song directory // parse song directory
BeatmapParser.parseAllFiles(beatmapDir); BeatmapParser.parseAllFiles(beatmapDir);
// import replays
ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir());
// load sounds // load sounds
SoundController.init(); SoundController.init();

View File

@ -41,6 +41,7 @@ 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.
@ -224,6 +225,7 @@ public class OpenALStreamPlayer {
*/ */
public void setup(float pitch) { public void setup(float pitch) {
this.pitch = pitch; this.pitch = pitch;
syncPosition();
} }
/** /**
@ -354,7 +356,7 @@ public class OpenALStreamPlayer {
} }
playedPos = streamPos; playedPos = streamPos;
syncStartTime = getTime() - playedPos * 1000 / sampleSize / sampleRate; syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate)/pitch);
startPlayback(); startPlayback();
@ -403,24 +405,34 @@ public class OpenALStreamPlayer {
float thisPosition = getALPosition(); float thisPosition = getALPosition();
long thisTime = getTime(); long thisTime = getTime();
float dxPosition = thisPosition - lastUpdatePosition; float dxPosition = thisPosition - lastUpdatePosition;
long dxTime = thisTime - syncStartTime; float dxTime = (thisTime - syncStartTime) * pitch;
// hard reset // hard reset
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) { if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
syncStartTime = thisTime - ((long) (thisPosition * 1000)); //System.out.println("Time HARD Reset"+" "+thisPosition+" "+(dxTime / 1000f));
dxTime = thisTime - syncStartTime; syncPosition();
dxTime = (thisTime - syncStartTime) * pitch;
avgDiff = 0; avgDiff = 0;
} }
if ((int) (dxPosition * 1000) != 0) { // lastPosition != thisPosition if ((int) (dxPosition * 1000) != 0) { // lastPosition != thisPosition
float diff = thisPosition * 1000 - (dxTime); float diff = thisPosition * 1000 - (dxTime);
avgDiff = (diff + avgDiff * 9) / 10; avgDiff = (diff + avgDiff * 9) / 10;
syncStartTime -= (int) (avgDiff/2); if(Math.abs(avgDiff) >= 1){
dxTime = thisTime - syncStartTime; syncStartTime -= (int)(avgDiff);
avgDiff -= (int)(avgDiff);
dxTime = (thisTime - syncStartTime) * pitch;
}
lastUpdatePosition = thisPosition; lastUpdatePosition = thisPosition;
} }
return dxTime / 1000f; return dxTime / 1000f;
} }
private void syncPosition(){
syncStartTime = getTime() - (long) ( getALPosition() * 1000 / pitch);
avgDiff = 0;
}
/** /**
* Processes a track pause. * Processes a track pause.

View File

@ -536,6 +536,7 @@ public class SoundStore {
*/ */
public void setMusicPitch(float pitch) { public void setMusicPitch(float pitch) {
if (soundWorks) { if (soundWorks) {
stream.setup(pitch);
AL10.alSourcef(sources.get(0), AL10.AL_PITCH, pitch); AL10.alSourcef(sources.get(0), AL10.AL_PITCH, pitch);
} }
} }