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++;
}
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:
* <p>
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
@ -1219,19 +1224,26 @@ public class GameData {
* <li><strong>Mod:</strong> mod multipliers
* </ul>
* @param hitValue the hit value
* @param hitObject
* @return the score value
*/
private int getScoreForHit(int hitValue) {
return hitValue + (int) (hitValue * (Math.max(combo - 1, 0) * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25);
private int getScoreForHit(int hitValue, HitObject hitObject) {
int comboMulti = Math.max(combo - 1, 0);
if(hitObject.isSlider()){
comboMulti += 1;
}
return (hitValue + (int)(hitValue * (comboMulti * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25));
}
/**
* https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier
* Computes and stores the difficulty multiplier used in the score formula.
* @param drainRate the raw HP drain rate value
* @param circleSize the raw circle size value
* @param overallDifficulty the raw overall difficulty value
*/
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) {
//TODO THE LIES ( difficultyMultiplier )
//*
float sum = drainRate + circleSize + overallDifficulty; // typically 2~27
if (sum <= 5f)
difficultyMultiplier = 2;
@ -1243,8 +1255,21 @@ public class GameData {
difficultyMultiplier = 5;
else //if (sum <= 30f)
difficultyMultiplier = 6;
//*/
/*
924 3x1/4 beat notes 0.14stars
924 3x1beat 0.28stars
912 3x1beat wth 1 extra note 10 sec away 0.29stars
seems to be based on hitobject density? (Total Objects/Time)
*/
/*
float mult = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f;
System.out.println("diffuculty Multiplier : "+ mult);
difficultyMultiplier = (int)mult;
*/
}
/**
* Handles a hit result and performs all associated calculations.
* @param time the object start time
@ -1292,7 +1317,7 @@ public class GameData {
hitObject.getAdditionSampleSet(repeat));
// calculate score and increment combo streak
changeScore(getScoreForHit(hitValue));
changeScore(getScoreForHit(hitValue, hitObject));
incrementComboStreak();
}
hitResultCount[result]++;
@ -1357,6 +1382,7 @@ public class GameData {
}
}
/**
* Returns a ScoreData object encapsulating all game data.
* If score data already exists, the existing object will be returned
@ -1387,6 +1413,7 @@ public class GameData {
scoreData.perfect = (comboMax == fullObjectCount);
scoreData.mods = GameMod.getModState();
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
scoreData.playerName = "OpsuPlayer"; //TODO GameDataPlayerName?
return scoreData;
}
@ -1407,7 +1434,7 @@ public class GameData {
replay = new Replay();
replay.mode = Beatmap.MODE_OSU;
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.replayHash = Long.toString(System.currentTimeMillis()); // TODO
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). */
private static File replayDir;
/** The replay import directory. */
private static File replayImportDir;
/** The root skin directory. */
private static File skinRootDir;
@ -1087,7 +1090,21 @@ public class Options {
oszDir.mkdir();
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.
* If invalid, this will return a "Screenshot" directory.

View File

@ -77,6 +77,9 @@ public class ScoreData implements Comparable<ScoreData> {
/** The tooltip string. */
private String tooltip;
/** The players Name. */
public String playerName;
/** Drawing values. */
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
@ -164,6 +167,7 @@ public class ScoreData implements Comparable<ScoreData> {
this.perfect = rs.getBoolean(16);
this.mods = rs.getInt(17);
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)
Utils.FONT_SMALL.drawString(
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
);
@ -331,6 +335,11 @@ public class ScoreData implements Comparable<ScoreData> {
);
}
public String getPlayerName() {
if(playerName == null)
return "Null Name";
return playerName;
}
@Override
public int compareTo(ScoreData that) {
if (this.score != that.score)

View File

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

View File

@ -19,16 +19,20 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.MD5InputStreamWrapper;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.db.BeatmapDB;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -205,7 +209,15 @@ public class BeatmapParser {
Beatmap beatmap = new Beatmap(file);
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 tokens[] = null;
while (line != null) {
@ -578,6 +590,8 @@ public class BeatmapParser {
break;
}
}
if (md5stream != null)
beatmap.md5Hash = md5stream.getMD5();
} catch (IOException e) {
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.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@ -57,6 +58,10 @@ public class BeatmapSetList {
/** Set of all beatmap set IDs for the parsed beatmaps. */
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). */
private int expandedIndex;
@ -83,6 +88,7 @@ public class BeatmapSetList {
private BeatmapSetList() {
parsedNodes = new ArrayList<BeatmapSetNode>();
MSIDdb = new HashSet<Integer>();
beatmapHashesToFile = new HashMap<String, Beatmap>();
reset();
}
@ -117,6 +123,10 @@ public class BeatmapSetList {
int msid = beatmaps.get(0).beatmapSetID;
if (msid > 0)
MSIDdb.add(msid);
for(Beatmap f : beatmaps) {
beatmapHashesToFile.put(f.md5Hash, f);
}
return node;
}
@ -501,4 +511,8 @@ public class BeatmapSetList {
* @return true if id is in the list
*/
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
public Beatmap getFileFromBeatmapHash(String beatmapHash) {
return beatmapHashesToFile.get(beatmapHash);
}
}

View File

@ -43,7 +43,7 @@ public class BeatmapDB {
* Current database version.
* 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. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@ -96,7 +96,7 @@ public class BeatmapDB {
insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectStmt = connection.prepareStatement("SELECT * 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, " +
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT" +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT," +
"md5hash TEXT" +
"); " +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
@ -340,6 +341,7 @@ public class BeatmapDB {
stmt.setString(38, beatmap.timingPointsToString());
stmt.setString(39, beatmap.breaksToString());
stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@ -481,6 +483,7 @@ public class BeatmapDB {
if (bg != null)
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = BeatmapParser.getDBString(rs.getString(41));
} catch (SQLException e) {
throw e;
} catch (Exception e) {

View File

@ -45,7 +45,7 @@ public class ScoreDB {
* This value should be changed whenever the database format changes.
* Add any update queries to the {@link #getUpdateQueries(int)} method.
*/
private static final int DATABASE_VERSION = 20140311;
private static final int DATABASE_VERSION = 20150401;
/**
* 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>();
if (version < 20140311)
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 */
@ -95,8 +97,13 @@ public class ScoreDB {
// prepare sql statements
try {
//TODO timestamp as primary key should prevent importing the same replay multiple times
//but if for some magical reason two different replays has the same time stamp
//it will fail, such as copying replays from another drive? which will reset
//the last modified of the file.
insertStmt = connection.prepareStatement(
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
@ -114,7 +121,8 @@ public class ScoreDB {
"DELETE FROM scores WHERE " +
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? 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) {
ErrorHandler.error("Failed to prepare score statements.", e, true);
@ -137,7 +145,8 @@ public class ScoreDB {
"combo INTEGER, " +
"perfect BOOLEAN, " +
"mods INTEGER, " +
"replay TEXT" +
"replay TEXT," +
"playerName TEXT"+
");" +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
@ -283,6 +292,9 @@ public class ScoreDB {
stmt.setInt(15, data.combo);
stmt.setBoolean(16, data.perfect);
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;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
@ -50,7 +51,7 @@ public class OsuReader {
* @param source the input stream to read from
*/
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;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
@ -51,7 +52,7 @@ public class OsuWriter {
* @param dest the output stream to write to
*/
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. */
private static final int FADE_IN_TIME = 375;
/** The diameter of Circle Hitobjects */
private static float diameter;
/** The associated HitObject. */
private HitObject hitObject;
@ -62,11 +65,12 @@ public class Circle implements GameObject {
* @param circleSize the map's circleSize value
*/
public static void init(GameContainer container, float circleSize) {
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter));
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
}
/**
@ -119,15 +123,16 @@ public class Circle implements GameObject {
private int hitResult(int time) {
int timeDiff = Math.abs(time);
int[] hitResultOffset = game.getHitResultOffsets();
int result = -1;
if (timeDiff < hitResultOffset[GameData.HIT_300])
if (timeDiff <= hitResultOffset[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;
else if (timeDiff < hitResultOffset[GameData.HIT_50])
else if (timeDiff <= hitResultOffset[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;
//else not a hit
@ -137,8 +142,7 @@ public class Circle implements GameObject {
@Override
public boolean mousePressed(int x, int y, int trackPosition) {
double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
if (distance < diameter/2) {
int timeDiff = trackPosition - hitObject.getTime();
int result = hitResult(timeDiff);
@ -158,7 +162,7 @@ public class Circle implements GameObject {
int[] hitResultOffset = game.getHitResultOffsets();
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
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.y = hitObject.getScaledY();
}
@Override
public void reset() {
}
}

View File

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

View File

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

View File

@ -49,6 +49,10 @@ public class Slider implements GameObject {
/** Rate at which slider ticks are placed. */
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. */
private static final int FADE_IN_TIME = 375;
@ -100,6 +104,8 @@ public class Slider implements GameObject {
/** Container dimensions. */
private static int containerWidth, containerHeight;
/**
* Initializes the Slider data type with images and dimensions.
@ -110,10 +116,12 @@ public class Slider implements GameObject {
public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
int diameterInt = (int)diameter;
followRadius = diameter / 2 * 3f;
// slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
@ -121,11 +129,11 @@ public class Slider implements GameObject {
else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
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.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameter / 4, diameter / 4));
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameterInt * 259 / 128, diameterInt * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4));
sliderMultiplier = beatmap.sliderMultiplier;
sliderTickRate = beatmap.sliderTickRate;
@ -272,6 +280,55 @@ public class Slider implements GameObject {
* @return the hit result (GameData.HIT_* constants)
*/
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;
int result;
@ -308,8 +365,7 @@ public class Slider implements GameObject {
return false;
double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
if (distance < diameter / 2) {
int timeDiff = Math.abs(trackPosition - hitObject.getTime());
int[] hitResultOffset = game.getHitResultOffsets();
@ -365,26 +421,29 @@ public class Slider implements GameObject {
}
// end of slider
if (overlap || trackPosition > hitObject.getTime() + sliderTimeTotal) {
if (trackPosition > hitObject.getTime() + sliderTimeTotal) {
tickIntervals++;
// check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) {
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
if (distance < followCircleRadius)
if (distance < followRadius)
sliderClickedFinal = true;
}
// final circle hit
if (sliderClickedFinal)
if (sliderClickedFinal){
ticksHit++;
data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats);
}
// "auto" mod: always send a perfect hit result
if (isAutoMod)
ticksHit = tickIntervals;
//TODO missing the final shouldn't increment the combo
// calculate and send slider result
hitResult();
return true;
@ -417,8 +476,7 @@ public class Slider implements GameObject {
// holding slider...
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) {
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
// mouse pressed and within follow circle
followCircleActive = true;
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;
}
}
@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;
/** The number of rotation velocities to store. */
// note: currently takes about 200ms to spin up (4 * 50)
private static final int MAX_ROTATION_VELOCITIES = 50;
private int maxStoredDeltaAngles;
/** 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. */
private static final int FADE_IN_TIME = 500;
@ -64,6 +63,8 @@ public class Spinner implements GameObject {
TWO_PI = (float) (Math.PI * 2),
HALF_PI = (float) (Math.PI / 2);
private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3
/** The associated HitObject. */
private HitObject hitObject;
@ -83,19 +84,25 @@ public class Spinner implements GameObject {
private float rotationsNeeded;
/** The remaining amount of time that was not used. */
private int deltaOverflow;
private float deltaOverflow;
/** The sum of all the velocities in storedVelocities. */
private float sumVelocity = 0f;
private float sumDeltaAngle = 0f;
/** 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. */
private boolean isSpinning;
/** 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.
@ -117,7 +124,46 @@ public class Spinner implements GameObject {
public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
this.data = data;
/*
1 beat = 731.707317073171ms
RPM at frame X with spinner Y beats long
10 20 30 40 50 60 <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
float spinsPerMinute = 100 + (overallDifficulty * 15);
rotationsNeeded = spinsPerMinute * (hitObject.getEndTime() - hitObject.getTime()) / 60000f;
@ -144,12 +190,11 @@ public class Spinner implements GameObject {
}
// rpm
int rpm = Math.abs(Math.round(sumVelocity / storedVelocities.length * 60));
Image rpmImg = GameImage.SPINNER_RPM.getImage();
rpmImg.setAlpha(alpha);
rpmImg.drawCentered(width / 2f, height - rpmImg.getHeight() / 2f);
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);
// spinner meter (subimage)
@ -205,7 +250,10 @@ public class Spinner implements GameObject {
}
@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
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
// http://osu.ppy.sh/wiki/FAQ#Spinners
float angle;
deltaOverflow += delta;
float angleDiff = 0;
if (GameMod.AUTO.isActive()) {
lastAngle = 0;
angle = delta * AUTO_MULTIPLIER;
angleDiff = delta * AUTO_MULTIPLIER;
isSpinning = true;
} else if (GameMod.SPUN_OUT.isActive() || GameMod.AUTOPILOT.isActive()) {
lastAngle = 0;
angle = delta * SPUN_OUT_MULTIPLIER;
angleDiff = delta * SPUN_OUT_MULTIPLIER;
isSpinning = true;
} 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
if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) {
@ -240,35 +289,52 @@ public class Spinner implements GameObject {
isSpinning = true;
return false;
}
angleDiff = angle - lastAngle;
if(Math.abs(angleDiff) > 0.01f){
lastAngle = angle;
}else{
angleDiff = 0;
}
}
// make angleDiff the smallest angle change possible
// (i.e. 1/4 rotation instead of 3/4 rotation)
float angleDiff = angle - lastAngle;
if (angleDiff < -Math.PI)
angleDiff += TWO_PI;
else if (angleDiff > Math.PI)
angleDiff -= TWO_PI;
// spin caused by the cursor
float cursorVelocity = 0;
//may be a problem at higher frame rate due to float point round off
if (isSpinning)
cursorVelocity = Utils.clamp(angleDiff / TWO_PI / delta * 1000, -8f, 8f);
deltaOverflow += delta;
deltaAngleOverflow += angleDiff;
while (deltaOverflow >= DELTA_UPDATE_TIME) {
sumVelocity -= storedVelocities[velocityIndex];
sumVelocity += cursorVelocity;
storedVelocities[velocityIndex++] = cursorVelocity;
velocityIndex %= storedVelocities.length;
// spin caused by the cursor
float deltaAngle = 0;
if (isSpinning){
deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
deltaAngleOverflow -= deltaAngle;
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
}
sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex];
sumDeltaAngle += deltaAngle;
storedDeltaAngle[deltaAngleIndex++] = deltaAngle;
deltaAngleIndex %= storedDeltaAngle.length;
deltaOverflow -= DELTA_UPDATE_TIME;
}
float rotationAngle = sumVelocity / storedVelocities.length * TWO_PI * delta / 1000;
rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles;
rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI;
lastAngle = angle;
drawnRPM = (int)(Math.abs(rotationPerSec * 60));
rotate(rotationAngle);
if (Math.abs(rotationAngle) > 0.00001f)
data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
}
//TODO may need to update 1 more time when the spinner ends?
return false;
}
@ -311,15 +377,38 @@ public class Spinner implements GameObject {
// added one whole rotation...
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
data.changeScore(1000);
SoundController.playSound(SoundEffect.SPINNERBONUS);
} else {
}
data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN);
}
/*
//The extra 100 for some spinners (mostly wrong)
if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) {
if (newRotations + 0.5f > rotationsNeeded) { // extra rotations
data.changeScore(100);
SoundController.playSound(SoundEffect.SPINNERSPIN);
}
}
//*/
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.Options;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.io.OsuReader;
import itdelatrisu.opsu.io.OsuWriter;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
@ -100,6 +104,8 @@ public class Replay {
/** Seed. (?) */
public int seed;
private ScoreData scoreData;
/** Seed string. */
private static final String SEED_STRING = "-12345";
@ -130,6 +136,46 @@ public class Replay {
reader.close();
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.
@ -232,7 +278,7 @@ public class Replay {
new Thread() {
@Override
public void run() {
try (FileOutputStream out = new FileOutputStream(file)) {
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
OsuWriter writer = new OsuWriter(out);
// 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 int state;
private int width;
private int height;
private boolean seeking;
public Game(int state) {
this.state = state;
}
@ -234,8 +240,8 @@ public class Game extends BasicGameState {
this.game = game;
input = container.getInput();
int width = container.getWidth();
int height = container.getHeight();
width = container.getWidth();
height = container.getHeight();
// create offscreen graphics
offscreen = new Image(width, height);
@ -609,6 +615,28 @@ public class Game extends BasicGameState {
// out of frames, use previous data
if (replayIndex >= replay.frames.length)
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
//TODO probably should to disable sounds then reseek to the new position
if(seeking && replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){
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
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
@ -753,7 +781,7 @@ public class Game extends BasicGameState {
while (objectIndex < gameObjects.length && trackPosition > beatmap.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time
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
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());
}
if(!GameMod.AUTO.isActive() && y < 50){
float pos = (float)x / width * beatmap.endTime;
MusicController.setPosition((int)pos);
seeking = true;
}
return;
}
@ -1044,6 +1077,9 @@ public class Game extends BasicGameState {
} else if (restart == Restart.REPLAY)
retries = 0;
gameObjects = new GameObject[beatmap.objects.length];
playbackSpeed = PlaybackSpeed.NORMAL;
// reset game data
resetGameData();
@ -1063,7 +1099,7 @@ public class Game extends BasicGameState {
// is this the last note in the combo?
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;
Color color = combo[hitObject.getComboIndex()];
@ -1278,7 +1314,6 @@ public class Game extends BasicGameState {
* Resets all game data and structures.
*/
public void resetGameData() {
gameObjects = new GameObject[beatmap.objects.length];
data.clear();
objectIndex = 0;
breakIndex = 0;
@ -1303,8 +1338,7 @@ public class Game extends BasicGameState {
autoMouseY = 0;
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
playbackSpeed = PlaybackSpeed.NORMAL;
System.gc();
}
@ -1405,11 +1439,19 @@ public class Game extends BasicGameState {
// overallDifficulty (hit result time offsets)
hitResultOffset = new int[GameData.HIT_MAX];
/*
float mult = 0.608f;
hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6))*mult);
hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8))*mult);
hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16))*mult);
hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10))*mult);
/*/
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
data.setHitResultOffset(hitResultOffset);
//*/
// HPDrainRate (health change)
data.setDrainRate(HPDrainRate);

View File

@ -25,6 +25,7 @@ import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.ui.UI;
@ -127,6 +128,9 @@ public class Splash extends BasicGameState {
// parse song directory
BeatmapParser.parseAllFiles(beatmapDir);
// import replays
ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir());
// load sounds
SoundController.init();

View File

@ -41,6 +41,7 @@ import org.lwjgl.openal.OpenALException;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL
* as required.
@ -224,6 +225,7 @@ public class OpenALStreamPlayer {
*/
public void setup(float pitch) {
this.pitch = pitch;
syncPosition();
}
/**
@ -354,7 +356,7 @@ public class OpenALStreamPlayer {
}
playedPos = streamPos;
syncStartTime = getTime() - playedPos * 1000 / sampleSize / sampleRate;
syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate)/pitch);
startPlayback();
@ -403,24 +405,34 @@ public class OpenALStreamPlayer {
float thisPosition = getALPosition();
long thisTime = getTime();
float dxPosition = thisPosition - lastUpdatePosition;
long dxTime = thisTime - syncStartTime;
float dxTime = (thisTime - syncStartTime) * pitch;
// hard reset
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
syncStartTime = thisTime - ((long) (thisPosition * 1000));
dxTime = thisTime - syncStartTime;
//System.out.println("Time HARD Reset"+" "+thisPosition+" "+(dxTime / 1000f));
syncPosition();
dxTime = (thisTime - syncStartTime) * pitch;
avgDiff = 0;
}
if ((int) (dxPosition * 1000) != 0) { // lastPosition != thisPosition
float diff = thisPosition * 1000 - (dxTime);
avgDiff = (diff + avgDiff * 9) / 10;
syncStartTime -= (int) (avgDiff/2);
dxTime = thisTime - syncStartTime;
if(Math.abs(avgDiff) >= 1){
syncStartTime -= (int)(avgDiff);
avgDiff -= (int)(avgDiff);
dxTime = (thisTime - syncStartTime) * pitch;
}
lastUpdatePosition = thisPosition;
}
return dxTime / 1000f;
}
private void syncPosition(){
syncStartTime = getTime() - (long) ( getALPosition() * 1000 / pitch);
avgDiff = 0;
}
/**
* Processes a track pause.

View File

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