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:
commit
7d08a7d391
|
@ -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];
|
||||||
|
|
91
src/itdelatrisu/opsu/MD5InputStreamWrapper.java
Normal file
91
src/itdelatrisu/opsu/MD5InputStreamWrapper.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
49
src/itdelatrisu/opsu/replay/ReplayImporter.java
Normal file
49
src/itdelatrisu/opsu/replay/ReplayImporter.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user