Follow-up to #99.
- Many code style changes. - Don't increment combo if missing the last slider circle. - Added player name in ranking screen. - Don't show null/default player names. - Only import replays with .osr extension. - Display loading status for importing replays. - Moved MD5InputStreamWrapper to package "opsu.io". Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
parent
7d08a7d391
commit
d860a30aed
|
@ -74,7 +74,7 @@ public class ErrorHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays an error popup and logs the given error.
|
* Displays an error popup and logs the given error.
|
||||||
* @param error a descR of the error
|
* @param error a description of the error
|
||||||
* @param e the exception causing the error
|
* @param e the exception causing the error
|
||||||
* @param report whether to ask to report the error
|
* @param report whether to ask to report the error
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -838,8 +838,9 @@ public class GameData {
|
||||||
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
|
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
|
||||||
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
|
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
|
||||||
String.format("Beatmap by %s", beatmap.creator), Color.white);
|
String.format("Beatmap by %s", beatmap.creator), Color.white);
|
||||||
|
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
|
||||||
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
|
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
|
||||||
String.format("Played on %s.", scoreData.getTimeString()), Color.white);
|
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
|
||||||
|
|
||||||
// mod icons
|
// mod icons
|
||||||
int modWidth = GameMod.AUTO.getImage().getWidth();
|
int modWidth = GameMod.AUTO.getImage().getWidth();
|
||||||
|
@ -1207,13 +1208,8 @@ public class GameData {
|
||||||
}
|
}
|
||||||
fullObjectCount++;
|
fullObjectCount++;
|
||||||
}
|
}
|
||||||
public void sliderFinalResult(int time, int hitSlider30, float x, float y,
|
|
||||||
HitObject hitObject, int currentRepeats) {
|
|
||||||
score += 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://osu.ppy.sh/wiki/Score
|
|
||||||
* Returns the score for a hit based on the following score formula:
|
* Returns the score for a hit based on the following score formula:
|
||||||
* <p>
|
* <p>
|
||||||
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
|
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
|
||||||
|
@ -1224,26 +1220,31 @@ public class GameData {
|
||||||
* <li><strong>Mod:</strong> mod multipliers
|
* <li><strong>Mod:</strong> mod multipliers
|
||||||
* </ul>
|
* </ul>
|
||||||
* @param hitValue the hit value
|
* @param hitValue the hit value
|
||||||
* @param hitObject
|
* @param hitObject the hit object
|
||||||
* @return the score value
|
* @return the score value
|
||||||
|
* @see <a href="https://osu.ppy.sh/wiki/Score">https://osu.ppy.sh/wiki/Score</a>
|
||||||
*/
|
*/
|
||||||
private int getScoreForHit(int hitValue, HitObject hitObject) {
|
private int getScoreForHit(int hitValue, HitObject hitObject) {
|
||||||
int comboMulti = Math.max(combo - 1, 0);
|
int comboMultiplier = Math.max(combo - 1, 0);
|
||||||
if(hitObject.isSlider()){
|
if (hitObject.isSlider())
|
||||||
comboMulti += 1;
|
comboMultiplier++;
|
||||||
}
|
return (hitValue + (int)(hitValue * (comboMultiplier * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25));
|
||||||
return (hitValue + (int)(hitValue * (comboMulti * difficultyMultiplier * GameMod.getScoreMultiplier()) / 25));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier
|
|
||||||
* Computes and stores the difficulty multiplier used in the score formula.
|
* Computes and stores the difficulty multiplier used in the score formula.
|
||||||
* @param drainRate the raw HP drain rate value
|
* @param drainRate the raw HP drain rate value
|
||||||
* @param circleSize the raw circle size value
|
* @param circleSize the raw circle size value
|
||||||
* @param overallDifficulty the raw overall difficulty value
|
* @param overallDifficulty the raw overall difficulty value
|
||||||
|
* @see <a href="https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier">https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier</a>
|
||||||
*/
|
*/
|
||||||
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) {
|
public void calculateDifficultyMultiplier(float drainRate, float circleSize, float overallDifficulty) {
|
||||||
//TODO THE LIES ( difficultyMultiplier )
|
// TODO: find the actual formula (osu!wiki is wrong)
|
||||||
//*
|
// seems to be based on hit object density? (total objects / time)
|
||||||
|
// 924 3x1/4 beat notes 0.14stars
|
||||||
|
// 924 3x1beat 0.28stars
|
||||||
|
// 912 3x1beat with 1 extra note 10 sec away 0.29stars
|
||||||
|
|
||||||
float sum = drainRate + circleSize + overallDifficulty; // typically 2~27
|
float sum = drainRate + circleSize + overallDifficulty; // typically 2~27
|
||||||
if (sum <= 5f)
|
if (sum <= 5f)
|
||||||
difficultyMultiplier = 2;
|
difficultyMultiplier = 2;
|
||||||
|
@ -1255,21 +1256,11 @@ public class GameData {
|
||||||
difficultyMultiplier = 5;
|
difficultyMultiplier = 5;
|
||||||
else //if (sum <= 30f)
|
else //if (sum <= 30f)
|
||||||
difficultyMultiplier = 6;
|
difficultyMultiplier = 6;
|
||||||
//*/
|
|
||||||
|
|
||||||
/*
|
//float multiplier = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f;
|
||||||
924 3x1/4 beat notes 0.14stars
|
//difficultyMultiplier = (int) multiplier;
|
||||||
924 3x1beat 0.28stars
|
|
||||||
912 3x1beat wth 1 extra note 10 sec away 0.29stars
|
|
||||||
|
|
||||||
seems to be based on hitobject density? (Total Objects/Time)
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
float mult = ((circleSize + overallDifficulty + drainRate) / 6) + 1.5f;
|
|
||||||
System.out.println("diffuculty Multiplier : "+ mult);
|
|
||||||
difficultyMultiplier = (int)mult;
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a hit result and performs all associated calculations.
|
* Handles a hit result and performs all associated calculations.
|
||||||
* @param time the object start time
|
* @param time the object start time
|
||||||
|
@ -1279,12 +1270,13 @@ public class GameData {
|
||||||
* @param color the combo color
|
* @param color the combo color
|
||||||
* @param end true if this is the last hit object in the combo
|
* @param end true if this is the last hit object in the combo
|
||||||
* @param hitObject the hit object
|
* @param hitObject the hit object
|
||||||
* @param repeat the current repeat number (for sliders, or 0 otherwise)
|
|
||||||
* @param hitResultType the type of hit object for the result
|
* @param hitResultType the type of hit object for the result
|
||||||
|
* @param repeat the current repeat number (for sliders, or 0 otherwise)
|
||||||
|
* @param noIncrementCombo if the combo should not be incremented by this result
|
||||||
* @return the actual hit result (HIT_* constants)
|
* @return the actual hit result (HIT_* constants)
|
||||||
*/
|
*/
|
||||||
private int handleHitResult(int time, int result, float x, float y, Color color,
|
private int handleHitResult(int time, int result, float x, float y, Color color, boolean end,
|
||||||
boolean end, HitObject hitObject, int repeat, HitObjectType hitResultType) {
|
HitObject hitObject, HitObjectType hitResultType, int repeat, boolean noIncrementCombo) {
|
||||||
// update health, score, and combo streak based on hit result
|
// update health, score, and combo streak based on hit result
|
||||||
int hitValue = 0;
|
int hitValue = 0;
|
||||||
switch (result) {
|
switch (result) {
|
||||||
|
@ -1318,7 +1310,8 @@ public class GameData {
|
||||||
|
|
||||||
// calculate score and increment combo streak
|
// calculate score and increment combo streak
|
||||||
changeScore(getScoreForHit(hitValue, hitObject));
|
changeScore(getScoreForHit(hitValue, hitObject));
|
||||||
incrementComboStreak();
|
if (!noIncrementCombo)
|
||||||
|
incrementComboStreak();
|
||||||
}
|
}
|
||||||
hitResultCount[result]++;
|
hitResultCount[result]++;
|
||||||
fullObjectCount++;
|
fullObjectCount++;
|
||||||
|
@ -1347,7 +1340,7 @@ public class GameData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a slider hit result.
|
* Handles a hit result.
|
||||||
* @param time the object start time
|
* @param time the object start time
|
||||||
* @param result the hit result (HIT_* constants)
|
* @param result the hit result (HIT_* constants)
|
||||||
* @param x the x coordinate
|
* @param x the x coordinate
|
||||||
|
@ -1355,15 +1348,17 @@ public class GameData {
|
||||||
* @param color the combo color
|
* @param color the combo color
|
||||||
* @param end true if this is the last hit object in the combo
|
* @param end true if this is the last hit object in the combo
|
||||||
* @param hitObject the hit object
|
* @param hitObject the hit object
|
||||||
* @param repeat the current repeat number (for sliders, or 0 otherwise)
|
|
||||||
* @param hitResultType the type of hit object for the result
|
* @param hitResultType the type of hit object for the result
|
||||||
* @param curve the slider curve (or null if not applicable)
|
|
||||||
* @param expand whether or not the hit result animation should expand (if applicable)
|
* @param expand whether or not the hit result animation should expand (if applicable)
|
||||||
|
* @param repeat the current repeat number (for sliders, or 0 otherwise)
|
||||||
|
* @param curve the slider curve (or null if not applicable)
|
||||||
|
* @param sliderHeldToEnd whether or not the slider was held to the end (if applicable)
|
||||||
*/
|
*/
|
||||||
public void hitResult(int time, int result, float x, float y, Color color,
|
public void hitResult(int time, int result, float x, float y, Color color,
|
||||||
boolean end, HitObject hitObject, int repeat,
|
boolean end, HitObject hitObject, HitObjectType hitResultType,
|
||||||
HitObjectType hitResultType, Curve curve, boolean expand) {
|
boolean expand, int repeat, Curve curve, boolean sliderHeldToEnd) {
|
||||||
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject, repeat, hitResultType);
|
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
|
||||||
|
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
|
||||||
|
|
||||||
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
|
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
|
||||||
; // hide perfect hit results
|
; // hide perfect hit results
|
||||||
|
@ -1382,7 +1377,6 @@ public class GameData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a ScoreData object encapsulating all game data.
|
* Returns a ScoreData object encapsulating all game data.
|
||||||
* If score data already exists, the existing object will be returned
|
* If score data already exists, the existing object will be returned
|
||||||
|
@ -1413,7 +1407,7 @@ public class GameData {
|
||||||
scoreData.perfect = (comboMax == fullObjectCount);
|
scoreData.perfect = (comboMax == fullObjectCount);
|
||||||
scoreData.mods = GameMod.getModState();
|
scoreData.mods = GameMod.getModState();
|
||||||
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
|
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
|
||||||
scoreData.playerName = "OpsuPlayer"; //TODO GameDataPlayerName?
|
scoreData.playerName = null; // TODO
|
||||||
return scoreData;
|
return scoreData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1434,7 +1428,7 @@ public class GameData {
|
||||||
replay = new Replay();
|
replay = new Replay();
|
||||||
replay.mode = Beatmap.MODE_OSU;
|
replay.mode = Beatmap.MODE_OSU;
|
||||||
replay.version = Updater.get().getBuildDate();
|
replay.version = Updater.get().getBuildDate();
|
||||||
replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash;//Utils.getMD5(beatmap.getFile());
|
replay.beatmapHash = (beatmap == null) ? "" : beatmap.md5Hash;
|
||||||
replay.playerName = ""; // TODO
|
replay.playerName = ""; // TODO
|
||||||
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
|
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
|
||||||
replay.hit300 = (short) hitResultCount[HIT_300];
|
replay.hit300 = (short) hitResultCount[HIT_300];
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
package itdelatrisu.opsu;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
public class MD5InputStreamWrapper extends InputStream {
|
|
||||||
|
|
||||||
InputStream in;
|
|
||||||
private boolean eof; // End Of File
|
|
||||||
MessageDigest md;
|
|
||||||
public MD5InputStreamWrapper(InputStream in) throws NoSuchAlgorithmException {
|
|
||||||
this.in = in;
|
|
||||||
md = MessageDigest.getInstance("MD5");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
int readed = in.read();
|
|
||||||
if(readed>=0)
|
|
||||||
md.update((byte) readed);
|
|
||||||
else
|
|
||||||
eof=true;
|
|
||||||
return readed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() throws IOException {
|
|
||||||
return in.available();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void mark(int readlimit) {
|
|
||||||
in.mark(readlimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean markSupported() {
|
|
||||||
return in.markSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
int readed = in.read(b, off, len);
|
|
||||||
if(readed>=0)
|
|
||||||
md.update(b, off, readed);
|
|
||||||
else
|
|
||||||
eof=true;
|
|
||||||
|
|
||||||
return readed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return read(b, 0 ,b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void reset() throws IOException {
|
|
||||||
throw new RuntimeException("MD5 stream not resetable");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long skip(long n) throws IOException {
|
|
||||||
throw new RuntimeException("MD5 stream not skipable");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMD5() throws IOException {
|
|
||||||
byte[] buf = null;
|
|
||||||
if(!eof)
|
|
||||||
buf = new byte[0x1000];
|
|
||||||
while(!eof){
|
|
||||||
read(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] md5byte = md.digest();
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
for (byte b : md5byte)
|
|
||||||
result.append(String.format("%02x", b));
|
|
||||||
//System.out.println("MD5 stream md5 " + result.toString());
|
|
||||||
return result.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -33,7 +33,7 @@ public class OszUnpacker {
|
||||||
/** The index of the current file being unpacked. */
|
/** The index of the current file being unpacked. */
|
||||||
private static int fileIndex = -1;
|
private static int fileIndex = -1;
|
||||||
|
|
||||||
/** The total number of directories to parse. */
|
/** The total number of files to unpack. */
|
||||||
private static File[] files;
|
private static File[] files;
|
||||||
|
|
||||||
// This class should not be instantiated.
|
// This class should not be instantiated.
|
||||||
|
|
|
@ -63,6 +63,9 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
/** The replay string. */
|
/** The replay string. */
|
||||||
public String replayString;
|
public String replayString;
|
||||||
|
|
||||||
|
/** The player name. */
|
||||||
|
public String playerName;
|
||||||
|
|
||||||
/** Time since the score was achieved. */
|
/** Time since the score was achieved. */
|
||||||
private String timeSince;
|
private String timeSince;
|
||||||
|
|
||||||
|
@ -78,9 +81,6 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
/** The tooltip string. */
|
/** The tooltip string. */
|
||||||
private String tooltip;
|
private String tooltip;
|
||||||
|
|
||||||
/** The players Name. */
|
|
||||||
public String playerName;
|
|
||||||
|
|
||||||
/** Drawing values. */
|
/** Drawing values. */
|
||||||
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
|
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset;
|
||||||
|
|
||||||
|
@ -262,9 +262,10 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// hit counts (custom: osu! shows user instead, above score)
|
// hit counts (custom: osu! shows user instead, above score)
|
||||||
|
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
|
||||||
Utils.FONT_SMALL.drawString(
|
Utils.FONT_SMALL.drawString(
|
||||||
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
|
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
|
||||||
String.format("300:%d 100:%d 50:%d Miss:%d Name:%s", hit300, hit100, hit50, miss, getPlayerName()),
|
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player),
|
||||||
Color.white
|
Color.white
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -335,11 +336,6 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPlayerName() {
|
|
||||||
if(playerName == null)
|
|
||||||
return "Null Name";
|
|
||||||
return playerName;
|
|
||||||
}
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(ScoreData that) {
|
public int compareTo(ScoreData that) {
|
||||||
if (this.score != that.score)
|
if (this.score != that.score)
|
||||||
|
|
|
@ -185,7 +185,7 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||||
/** Slider border color. If null, the skin value is used. */
|
/** Slider border color. If null, the skin value is used. */
|
||||||
public Color sliderBorder;
|
public Color sliderBorder;
|
||||||
|
|
||||||
/** md5 hash of this file */
|
/** MD5 hash of this file. */
|
||||||
public String md5Hash;
|
public String md5Hash;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,9 +19,9 @@
|
||||||
package itdelatrisu.opsu.beatmap;
|
package itdelatrisu.opsu.beatmap;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
import itdelatrisu.opsu.MD5InputStreamWrapper;
|
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.db.BeatmapDB;
|
import itdelatrisu.opsu.db.BeatmapDB;
|
||||||
|
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
@ -68,6 +68,9 @@ public class BeatmapParser {
|
||||||
/** The current status. */
|
/** The current status. */
|
||||||
private static Status status = Status.NONE;
|
private static Status status = Status.NONE;
|
||||||
|
|
||||||
|
/** If no Provider supports a MessageDigestSpi implementation for the MD5 algorithm. */
|
||||||
|
private static boolean hasNoMD5Algorithm = false;
|
||||||
|
|
||||||
// This class should not be instantiated.
|
// This class should not be instantiated.
|
||||||
private BeatmapParser() {}
|
private BeatmapParser() {}
|
||||||
|
|
||||||
|
@ -209,15 +212,11 @@ public class BeatmapParser {
|
||||||
Beatmap beatmap = new Beatmap(file);
|
Beatmap beatmap = new Beatmap(file);
|
||||||
beatmap.timingPoints = new ArrayList<TimingPoint>();
|
beatmap.timingPoints = new ArrayList<TimingPoint>();
|
||||||
|
|
||||||
try (InputStream inFileStream = new BufferedInputStream(new FileInputStream(file));){
|
try (
|
||||||
MD5InputStreamWrapper md5stream = null;
|
InputStream bis = new BufferedInputStream(new FileInputStream(file));
|
||||||
try {
|
MD5InputStreamWrapper md5stream = (!hasNoMD5Algorithm) ? new MD5InputStreamWrapper(bis) : null;
|
||||||
md5stream = new MD5InputStreamWrapper(inFileStream);
|
BufferedReader in = new BufferedReader(new InputStreamReader((md5stream != null) ? md5stream : bis, "UTF-8"));
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
) {
|
||||||
ErrorHandler.error("Failed to get MD5 hash stream.", e1, true);
|
|
||||||
}
|
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(md5stream!=null?md5stream:inFileStream, "UTF-8"));
|
|
||||||
|
|
||||||
String line = in.readLine();
|
String line = in.readLine();
|
||||||
String tokens[] = null;
|
String tokens[] = null;
|
||||||
while (line != null) {
|
while (line != null) {
|
||||||
|
@ -594,6 +593,12 @@ public class BeatmapParser {
|
||||||
beatmap.md5Hash = md5stream.getMD5();
|
beatmap.md5Hash = md5stream.getMD5();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false);
|
ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
ErrorHandler.error("Failed to get MD5 hash stream.", e, true);
|
||||||
|
|
||||||
|
// retry without MD5
|
||||||
|
hasNoMD5Algorithm = true;
|
||||||
|
return parseFile(file, dir, beatmaps, parseObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
// no associated audio file?
|
// no associated audio file?
|
||||||
|
|
|
@ -59,9 +59,8 @@ public class BeatmapSetList {
|
||||||
/** Set of all beatmap set IDs for the parsed beatmaps. */
|
/** Set of all beatmap set IDs for the parsed beatmaps. */
|
||||||
private HashSet<Integer> MSIDdb;
|
private HashSet<Integer> MSIDdb;
|
||||||
|
|
||||||
/** Map of all hash to Beatmap . */
|
/** Map of all MD5 hashes to beatmaps. */
|
||||||
public HashMap<String, Beatmap> beatmapHashesToFile;
|
private HashMap<String, Beatmap> beatmapHashDB;
|
||||||
|
|
||||||
|
|
||||||
/** Index of current expanded node (-1 if no node is expanded). */
|
/** Index of current expanded node (-1 if no node is expanded). */
|
||||||
private int expandedIndex;
|
private int expandedIndex;
|
||||||
|
@ -88,7 +87,7 @@ public class BeatmapSetList {
|
||||||
private BeatmapSetList() {
|
private BeatmapSetList() {
|
||||||
parsedNodes = new ArrayList<BeatmapSetNode>();
|
parsedNodes = new ArrayList<BeatmapSetNode>();
|
||||||
MSIDdb = new HashSet<Integer>();
|
MSIDdb = new HashSet<Integer>();
|
||||||
beatmapHashesToFile = new HashMap<String, Beatmap>();
|
beatmapHashDB = new HashMap<String, Beatmap>();
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,10 +122,12 @@ public class BeatmapSetList {
|
||||||
int msid = beatmaps.get(0).beatmapSetID;
|
int msid = beatmaps.get(0).beatmapSetID;
|
||||||
if (msid > 0)
|
if (msid > 0)
|
||||||
MSIDdb.add(msid);
|
MSIDdb.add(msid);
|
||||||
for(Beatmap f : beatmaps) {
|
|
||||||
beatmapHashesToFile.put(f.md5Hash, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// add MD5 hashes to table
|
||||||
|
for (Beatmap beatmap : beatmaps) {
|
||||||
|
if (beatmap.md5Hash != null)
|
||||||
|
beatmapHashDB.put(beatmap.md5Hash, beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -512,7 +513,12 @@ public class BeatmapSetList {
|
||||||
*/
|
*/
|
||||||
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
|
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
|
||||||
|
|
||||||
public Beatmap getFileFromBeatmapHash(String beatmapHash) {
|
/**
|
||||||
return beatmapHashesToFile.get(beatmapHash);
|
* Returns the beatmap associated with the given hash.
|
||||||
|
* @param beatmapHash the MD5 hash
|
||||||
|
* @return the associated beatmap, or {@code null} if no match was found
|
||||||
|
*/
|
||||||
|
public Beatmap getBeatmapFromHash(String beatmapHash) {
|
||||||
|
return beatmapHashDB.get(beatmapHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -122,7 +122,7 @@ public class BeatmapDB {
|
||||||
"bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
|
"bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
|
||||||
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
||||||
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
||||||
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT," +
|
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
|
||||||
"md5hash TEXT" +
|
"md5hash TEXT" +
|
||||||
"); " +
|
"); " +
|
||||||
"CREATE TABLE IF NOT EXISTS info (" +
|
"CREATE TABLE IF NOT EXISTS info (" +
|
||||||
|
@ -483,7 +483,7 @@ public class BeatmapDB {
|
||||||
if (bg != null)
|
if (bg != null)
|
||||||
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
||||||
beatmap.sliderBorderFromString(rs.getString(37));
|
beatmap.sliderBorderFromString(rs.getString(37));
|
||||||
beatmap.md5Hash = BeatmapParser.getDBString(rs.getString(41));
|
beatmap.md5Hash = rs.getString(41);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -97,12 +97,9 @@ public class ScoreDB {
|
||||||
|
|
||||||
// prepare sql statements
|
// prepare sql statements
|
||||||
try {
|
try {
|
||||||
|
|
||||||
//TODO timestamp as primary key should prevent importing the same replay multiple times
|
|
||||||
//but if for some magical reason two different replays has the same time stamp
|
|
||||||
//it will fail, such as copying replays from another drive? which will reset
|
|
||||||
//the last modified of the file.
|
|
||||||
insertStmt = connection.prepareStatement(
|
insertStmt = connection.prepareStatement(
|
||||||
|
// TODO: There will be problems if multiple replays have the same
|
||||||
|
// timestamp (e.g. when imported) due to timestamp being the primary key.
|
||||||
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
);
|
);
|
||||||
selectMapStmt = connection.prepareStatement(
|
selectMapStmt = connection.prepareStatement(
|
||||||
|
@ -145,7 +142,7 @@ public class ScoreDB {
|
||||||
"combo INTEGER, " +
|
"combo INTEGER, " +
|
||||||
"perfect BOOLEAN, " +
|
"perfect BOOLEAN, " +
|
||||||
"mods INTEGER, " +
|
"mods INTEGER, " +
|
||||||
"replay TEXT," +
|
"replay TEXT, " +
|
||||||
"playerName TEXT"+
|
"playerName TEXT"+
|
||||||
");" +
|
");" +
|
||||||
"CREATE TABLE IF NOT EXISTS info (" +
|
"CREATE TABLE IF NOT EXISTS info (" +
|
||||||
|
@ -294,7 +291,6 @@ public class ScoreDB {
|
||||||
stmt.setInt(17, data.mods);
|
stmt.setInt(17, data.mods);
|
||||||
stmt.setString(18, data.replayString);
|
stmt.setString(18, data.replayString);
|
||||||
stmt.setString(19, data.playerName);
|
stmt.setString(19, data.playerName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
118
src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java
Normal file
118
src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package itdelatrisu.opsu.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for an InputStream that computes the MD5 hash while reading the stream.
|
||||||
|
*/
|
||||||
|
public class MD5InputStreamWrapper extends InputStream {
|
||||||
|
/** The input stream. */
|
||||||
|
private InputStream in;
|
||||||
|
|
||||||
|
/** Whether the end of stream has been reached. */
|
||||||
|
private boolean eof = false;
|
||||||
|
|
||||||
|
/** A MessageDigest object that implements the MD5 digest algorithm. */
|
||||||
|
private MessageDigest md;
|
||||||
|
|
||||||
|
/** The computed MD5 hash. */
|
||||||
|
private String md5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param in the input stream
|
||||||
|
* @throws NoSuchAlgorithmException if no Provider supports a MessageDigestSpi implementation for the MD5 algorithm
|
||||||
|
*/
|
||||||
|
public MD5InputStreamWrapper(InputStream in) throws NoSuchAlgorithmException {
|
||||||
|
this.in = in;
|
||||||
|
this.md = MessageDigest.getInstance("MD5");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
int bytesRead = in.read();
|
||||||
|
if (bytesRead >= 0)
|
||||||
|
md.update((byte) bytesRead);
|
||||||
|
else
|
||||||
|
eof = true;
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException { return in.available(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException { in.close(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) { in.mark(readlimit); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() { return in.markSupported(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
int bytesRead = in.read(b, off, len);
|
||||||
|
if (bytesRead >= 0)
|
||||||
|
md.update(b, off, bytesRead);
|
||||||
|
else
|
||||||
|
eof = true;
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
throw new RuntimeException("The reset() method is not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
throw new RuntimeException("The skip() method is not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MD5 hash of the input stream.
|
||||||
|
* @throws IOException if the end of stream has not yet been reached and a call to {@link #read(byte[])} fails
|
||||||
|
*/
|
||||||
|
public String getMD5() throws IOException {
|
||||||
|
if (md5 != null)
|
||||||
|
return md5;
|
||||||
|
|
||||||
|
if (!eof) { // read the rest of the stream
|
||||||
|
byte[] buf = new byte[0x1000];
|
||||||
|
while (!eof)
|
||||||
|
read(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] md5byte = md.digest();
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (byte b : md5byte)
|
||||||
|
result.append(String.format("%02x", b));
|
||||||
|
md5 = result.toString();
|
||||||
|
return md5;
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ public class Circle implements GameObject {
|
||||||
/** The amount of time, in milliseconds, to fade in the circle. */
|
/** The amount of time, in milliseconds, to fade in the circle. */
|
||||||
private static final int FADE_IN_TIME = 375;
|
private static final int FADE_IN_TIME = 375;
|
||||||
|
|
||||||
/** The diameter of Circle Hitobjects */
|
/** The diameter of hit circles. */
|
||||||
private static float diameter;
|
private static float diameter;
|
||||||
|
|
||||||
/** The associated HitObject. */
|
/** The associated HitObject. */
|
||||||
|
@ -65,9 +65,9 @@ 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) {
|
||||||
diameter = (104 - (circleSize * 8));
|
diameter = (104 - (circleSize * 8));
|
||||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||||
int diameterInt = (int)diameter;
|
int diameterInt = (int) diameter;
|
||||||
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||||
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
|
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||||
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||||
|
@ -123,7 +123,6 @@ public class Circle implements GameObject {
|
||||||
private int hitResult(int time) {
|
private int hitResult(int time) {
|
||||||
int timeDiff = Math.abs(time);
|
int timeDiff = Math.abs(time);
|
||||||
|
|
||||||
|
|
||||||
int[] hitResultOffset = game.getHitResultOffsets();
|
int[] hitResultOffset = game.getHitResultOffsets();
|
||||||
int result = -1;
|
int result = -1;
|
||||||
if (timeDiff <= hitResultOffset[GameData.HIT_300])
|
if (timeDiff <= hitResultOffset[GameData.HIT_300])
|
||||||
|
@ -142,13 +141,13 @@ public class Circle implements GameObject {
|
||||||
@Override
|
@Override
|
||||||
public boolean mousePressed(int x, int y, int trackPosition) {
|
public boolean mousePressed(int x, int y, int trackPosition) {
|
||||||
double distance = Math.hypot(this.x - x, this.y - y);
|
double distance = Math.hypot(this.x - x, this.y - y);
|
||||||
if (distance < diameter/2) {
|
if (distance < diameter / 2) {
|
||||||
int timeDiff = trackPosition - hitObject.getTime();
|
int timeDiff = trackPosition - hitObject.getTime();
|
||||||
int result = hitResult(timeDiff);
|
int result = hitResult(timeDiff);
|
||||||
|
|
||||||
if (result > -1) {
|
if (result > -1) {
|
||||||
data.addHitError(hitObject.getTime(), x, y, timeDiff);
|
data.addHitError(hitObject.getTime(), x, y, timeDiff);
|
||||||
data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
|
data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,17 +163,17 @@ public class Circle implements GameObject {
|
||||||
|
|
||||||
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
|
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
|
||||||
if (isAutoMod) // "auto" mod: catch any missed notes due to lag
|
if (isAutoMod) // "auto" mod: catch any missed notes due to lag
|
||||||
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
|
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
|
||||||
|
|
||||||
else // no more points can be scored, so send a miss
|
else // no more points can be scored, so send a miss
|
||||||
data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
|
data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "auto" mod: send a perfect hit result
|
// "auto" mod: send a perfect hit result
|
||||||
else if (isAutoMod) {
|
else if (isAutoMod) {
|
||||||
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
|
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
|
||||||
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
|
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, HitObjectType.CIRCLE, true, 0, null, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,7 +198,5 @@ public class Circle implements GameObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,5 @@ public class DummyObject implements GameObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,7 @@ public interface GameObject {
|
||||||
public void updatePosition();
|
public void updatePosition();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the hit object so that it can be reused.
|
* Resets all internal state so that the hit object can be reused.
|
||||||
*/
|
*/
|
||||||
public void reset();
|
public void reset();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,10 @@ public class Slider implements GameObject {
|
||||||
/** Rate at which slider ticks are placed. */
|
/** Rate at which slider ticks are placed. */
|
||||||
private static float sliderTickRate = 1.0f;
|
private static float sliderTickRate = 1.0f;
|
||||||
|
|
||||||
|
/** Follow circle radius. */
|
||||||
private static float followRadius;
|
private static float followRadius;
|
||||||
|
|
||||||
|
/** The diameter of hit circles. */
|
||||||
private static float diameter;
|
private static float diameter;
|
||||||
|
|
||||||
/** The amount of time, in milliseconds, to fade in the slider. */
|
/** The amount of time, in milliseconds, to fade in the slider. */
|
||||||
|
@ -81,8 +83,11 @@ public class Slider implements GameObject {
|
||||||
/** The time duration of the slider including repeats, in milliseconds. */
|
/** The time duration of the slider including repeats, in milliseconds. */
|
||||||
private float sliderTimeTotal = 0f;
|
private float sliderTimeTotal = 0f;
|
||||||
|
|
||||||
/** Whether or not the result of the initial/final hit circles have been processed. */
|
/** Whether or not the result of the initial hit circle has been processed. */
|
||||||
private boolean sliderClickedInitial = false, sliderClickedFinal = false;
|
private boolean sliderClickedInitial = false;
|
||||||
|
|
||||||
|
/** Whether or not the slider was held to the end. */
|
||||||
|
private boolean sliderHeldToEnd = false;
|
||||||
|
|
||||||
/** Whether or not to show the follow circle. */
|
/** Whether or not to show the follow circle. */
|
||||||
private boolean followCircleActive = false;
|
private boolean followCircleActive = false;
|
||||||
|
@ -105,8 +110,6 @@ public class Slider implements GameObject {
|
||||||
/** Container dimensions. */
|
/** Container dimensions. */
|
||||||
private static int containerWidth, containerHeight;
|
private static int containerWidth, containerHeight;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the Slider data type with images and dimensions.
|
* Initializes the Slider data type with images and dimensions.
|
||||||
* @param container the game container
|
* @param container the game container
|
||||||
|
@ -117,11 +120,12 @@ public class Slider implements GameObject {
|
||||||
containerWidth = container.getWidth();
|
containerWidth = container.getWidth();
|
||||||
containerHeight = container.getHeight();
|
containerHeight = container.getHeight();
|
||||||
|
|
||||||
diameter = (104 - (circleSize * 8));
|
diameter = (104 - (circleSize * 8));
|
||||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||||
int diameterInt = (int)diameter;
|
int diameterInt = (int) diameter;
|
||||||
|
|
||||||
followRadius = diameter / 2 * 3f;
|
followRadius = diameter / 2 * 3f;
|
||||||
|
|
||||||
// slider ball
|
// slider ball
|
||||||
if (GameImage.SLIDER_BALL.hasSkinImages() ||
|
if (GameImage.SLIDER_BALL.hasSkinImages() ||
|
||||||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
||||||
|
@ -326,8 +330,6 @@ public class Slider implements GameObject {
|
||||||
80 - 10*3 - 50 - 0 (3 tick only)(0x)
|
80 - 10*3 - 50 - 0 (3 tick only)(0x)
|
||||||
70 - 10*2 - 50 - 0 (2 tick only)(0x)
|
70 - 10*2 - 50 - 0 (2 tick only)(0x)
|
||||||
60 - 10 - 50 - 0 (1 tick only)(0x)
|
60 - 10 - 50 - 0 (1 tick only)(0x)
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
float tickRatio = (float) ticksHit / tickIntervals;
|
float tickRatio = (float) ticksHit / tickIntervals;
|
||||||
|
|
||||||
|
@ -354,7 +356,8 @@ public class Slider implements GameObject {
|
||||||
type = HitObjectType.SLIDER_FIRST;
|
type = HitObjectType.SLIDER_FIRST;
|
||||||
}
|
}
|
||||||
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
|
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
|
||||||
cx, cy, color, comboEnd, hitObject, currentRepeats + 1, type, curve, sliderClickedFinal);
|
cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd,
|
||||||
|
currentRepeats + 1, curve, sliderHeldToEnd);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -429,21 +432,17 @@ public class Slider implements GameObject {
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||||
if (distance < followRadius)
|
if (distance < followRadius)
|
||||||
sliderClickedFinal = true;
|
sliderHeldToEnd = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// final circle hit
|
// final circle hit
|
||||||
if (sliderClickedFinal){
|
if (sliderHeldToEnd)
|
||||||
ticksHit++;
|
ticksHit++;
|
||||||
data.sliderFinalResult(hitObject.getTime(), GameData.HIT_SLIDER30, this.x, this.y, hitObject, currentRepeats);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "auto" mod: always send a perfect hit result
|
// "auto" mod: always send a perfect hit result
|
||||||
if (isAutoMod)
|
if (isAutoMod)
|
||||||
ticksHit = tickIntervals;
|
ticksHit = tickIntervals;
|
||||||
|
|
||||||
//TODO missing the final shouldn't increment the combo
|
|
||||||
|
|
||||||
// calculate and send slider result
|
// calculate and send slider result
|
||||||
hitResult();
|
hitResult();
|
||||||
return true;
|
return true;
|
||||||
|
@ -501,8 +500,8 @@ public class Slider implements GameObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// held near end of slider
|
// held near end of slider
|
||||||
if (!sliderClickedFinal && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300])
|
if (!sliderHeldToEnd && trackPosition > hitObject.getTime() + sliderTimeTotal - hitResultOffset[GameData.HIT_300])
|
||||||
sliderClickedFinal = true;
|
sliderHeldToEnd = true;
|
||||||
} else {
|
} else {
|
||||||
followCircleActive = false;
|
followCircleActive = false;
|
||||||
|
|
||||||
|
@ -563,12 +562,11 @@ public class Slider implements GameObject {
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {
|
||||||
sliderClickedInitial = false;
|
sliderClickedInitial = false;
|
||||||
sliderClickedFinal = false;
|
sliderHeldToEnd = false;
|
||||||
followCircleActive = false;
|
followCircleActive = false;
|
||||||
currentRepeats = 0;
|
currentRepeats = 0;
|
||||||
tickIndex = 0;
|
tickIndex = 0;
|
||||||
ticksHit = 0;
|
ticksHit = 0;
|
||||||
tickIntervals = 1;
|
tickIntervals = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,13 +58,14 @@ public class Spinner implements GameObject {
|
||||||
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
|
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
|
||||||
SPUN_OUT_MULTIPLIER = 1 / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI;
|
SPUN_OUT_MULTIPLIER = 1 / 33.25f; // angle = 287/60f * delta/1000f * TWO_PI;
|
||||||
|
|
||||||
|
/** Maximum angle difference. */
|
||||||
|
private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3
|
||||||
|
|
||||||
/** PI constants. */
|
/** PI constants. */
|
||||||
private static final float
|
private static final float
|
||||||
TWO_PI = (float) (Math.PI * 2),
|
TWO_PI = (float) (Math.PI * 2),
|
||||||
HALF_PI = (float) (Math.PI / 2);
|
HALF_PI = (float) (Math.PI / 2);
|
||||||
|
|
||||||
private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * AUTO_MULTIPLIER; // ~95.3
|
|
||||||
|
|
||||||
/** The associated HitObject. */
|
/** The associated HitObject. */
|
||||||
private HitObject hitObject;
|
private HitObject hitObject;
|
||||||
|
|
||||||
|
@ -124,6 +125,7 @@ public class Spinner implements GameObject {
|
||||||
public Spinner(HitObject hitObject, Game game, GameData data) {
|
public Spinner(HitObject hitObject, Game game, GameData data) {
|
||||||
this.hitObject = hitObject;
|
this.hitObject = hitObject;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
1 beat = 731.707317073171ms
|
1 beat = 731.707317073171ms
|
||||||
RPM at frame X with spinner Y beats long
|
RPM at frame X with spinner Y beats long
|
||||||
|
@ -152,16 +154,16 @@ public class Spinner implements GameObject {
|
||||||
25.00 53 154 272 391 447 466
|
25.00 53 154 272 391 447 466
|
||||||
^beats
|
^beats
|
||||||
*/
|
*/
|
||||||
//TODO not correct at all, but close enough?
|
// TODO not correct at all, but close enough?
|
||||||
//<2sec ~ 12 ~ 200ms
|
// <2sec ~ 12 ~ 200ms
|
||||||
//>5sec ~ 48 ~ 800ms
|
// >5sec ~ 48 ~ 800ms
|
||||||
|
|
||||||
final int minVel = 12;
|
final int minVel = 12;
|
||||||
final int maxVel = 48;
|
final int maxVel = 48;
|
||||||
final int minTime = 2000;
|
final int minTime = 2000;
|
||||||
final int maxTime = 5000;
|
final int maxTime = 5000;
|
||||||
maxStoredDeltaAngles = (int) Utils.clamp(
|
maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
|
||||||
(hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel-minVel)/(maxTime-minTime) + minVel
|
* (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
|
||||||
, minVel, maxVel);
|
|
||||||
storedDeltaAngle = new float[maxStoredDeltaAngles];
|
storedDeltaAngle = new float[maxStoredDeltaAngles];
|
||||||
|
|
||||||
// calculate rotations needed
|
// calculate rotations needed
|
||||||
|
@ -245,7 +247,7 @@ public class Spinner implements GameObject {
|
||||||
result = GameData.HIT_MISS;
|
result = GameData.HIT_MISS;
|
||||||
|
|
||||||
data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
|
data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
|
||||||
Color.transparent, true, hitObject, 0, HitObjectType.SPINNER, null, true);
|
Color.transparent, true, hitObject, HitObjectType.SPINNER, true, 0, null, false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +259,6 @@ public class Spinner implements GameObject {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
|
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
|
||||||
|
|
||||||
// end of spinner
|
// end of spinner
|
||||||
if (overlap || trackPosition > hitObject.getEndTime()) {
|
if (overlap || trackPosition > hitObject.getEndTime()) {
|
||||||
hitResult();
|
hitResult();
|
||||||
|
@ -289,13 +290,12 @@ public class Spinner implements GameObject {
|
||||||
isSpinning = true;
|
isSpinning = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
angleDiff = angle - lastAngle;
|
|
||||||
if(Math.abs(angleDiff) > 0.01f){
|
|
||||||
lastAngle = angle;
|
|
||||||
}else{
|
|
||||||
angleDiff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
angleDiff = angle - lastAngle;
|
||||||
|
if (Math.abs(angleDiff) > 0.01f)
|
||||||
|
lastAngle = angle;
|
||||||
|
else
|
||||||
|
angleDiff = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make angleDiff the smallest angle change possible
|
// make angleDiff the smallest angle change possible
|
||||||
|
@ -305,17 +305,17 @@ public class Spinner implements GameObject {
|
||||||
else if (angleDiff > Math.PI)
|
else if (angleDiff > Math.PI)
|
||||||
angleDiff -= TWO_PI;
|
angleDiff -= TWO_PI;
|
||||||
|
|
||||||
//may be a problem at higher frame rate due to float point round off
|
// may be a problem at higher frame rate due to floating point round off
|
||||||
if (isSpinning)
|
if (isSpinning)
|
||||||
deltaAngleOverflow += angleDiff;
|
deltaAngleOverflow += angleDiff;
|
||||||
|
|
||||||
while (deltaOverflow >= DELTA_UPDATE_TIME) {
|
while (deltaOverflow >= DELTA_UPDATE_TIME) {
|
||||||
// spin caused by the cursor
|
// spin caused by the cursor
|
||||||
float deltaAngle = 0;
|
float deltaAngle = 0;
|
||||||
if (isSpinning){
|
if (isSpinning) {
|
||||||
deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
|
deltaAngle = deltaAngleOverflow * DELTA_UPDATE_TIME / deltaOverflow;
|
||||||
deltaAngleOverflow -= deltaAngle;
|
deltaAngleOverflow -= deltaAngle;
|
||||||
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
|
deltaAngle = Utils.clamp(deltaAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
|
||||||
}
|
}
|
||||||
sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex];
|
sumDeltaAngle -= storedDeltaAngle[deltaAngleIndex];
|
||||||
sumDeltaAngle += deltaAngle;
|
sumDeltaAngle += deltaAngle;
|
||||||
|
@ -324,16 +324,16 @@ public class Spinner implements GameObject {
|
||||||
deltaOverflow -= DELTA_UPDATE_TIME;
|
deltaOverflow -= DELTA_UPDATE_TIME;
|
||||||
|
|
||||||
float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles;
|
float rotationAngle = sumDeltaAngle / maxStoredDeltaAngles;
|
||||||
rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
|
rotationAngle = Utils.clamp(rotationAngle, -MAX_ANG_DIFF, MAX_ANG_DIFF);
|
||||||
float rotationPerSec = rotationAngle * (1000/DELTA_UPDATE_TIME) / TWO_PI;
|
float rotationPerSec = rotationAngle * (1000 / DELTA_UPDATE_TIME) / TWO_PI;
|
||||||
|
|
||||||
drawnRPM = (int)(Math.abs(rotationPerSec * 60));
|
drawnRPM = (int) (Math.abs(rotationPerSec * 60));
|
||||||
|
|
||||||
rotate(rotationAngle);
|
rotate(rotationAngle);
|
||||||
if (Math.abs(rotationAngle) > 0.00001f)
|
if (Math.abs(rotationAngle) > 0.00001f)
|
||||||
data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
|
data.changeHealth(DELTA_UPDATE_TIME * GameData.HP_DRAIN_MULTIPLIER);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO may need to update 1 more time when the spinner ends?
|
//TODO may need to update 1 more time when the spinner ends?
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -380,21 +380,17 @@ public class Spinner implements GameObject {
|
||||||
//TODO seems to give 1100 points per spin but also an extra 100 for some spinners
|
//TODO seems to give 1100 points per spin but also an extra 100 for some spinners
|
||||||
if (newRotations > rotationsNeeded) { // extra rotations
|
if (newRotations > rotationsNeeded) { // extra rotations
|
||||||
data.changeScore(1000);
|
data.changeScore(1000);
|
||||||
|
|
||||||
SoundController.playSound(SoundEffect.SPINNERBONUS);
|
SoundController.playSound(SoundEffect.SPINNERBONUS);
|
||||||
}
|
}
|
||||||
data.changeScore(100);
|
data.changeScore(100);
|
||||||
SoundController.playSound(SoundEffect.SPINNERSPIN);
|
SoundController.playSound(SoundEffect.SPINNERSPIN);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
// extra 100 for some spinners (mostly wrong)
|
||||||
/*
|
// if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) {
|
||||||
//The extra 100 for some spinners (mostly wrong)
|
// if (newRotations + 0.5f > rotationsNeeded) // extra rotations
|
||||||
if (Math.floor(newRotations + 0.5f) > rotations + 0.5f) {
|
// data.changeScore(100);
|
||||||
if (newRotations + 0.5f > rotationsNeeded) { // extra rotations
|
// }
|
||||||
data.changeScore(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//*/
|
|
||||||
|
|
||||||
rotations = newRotations;
|
rotations = newRotations;
|
||||||
}
|
}
|
||||||
|
@ -403,9 +399,8 @@ public class Spinner implements GameObject {
|
||||||
public void reset() {
|
public void reset() {
|
||||||
deltaAngleIndex = 0;
|
deltaAngleIndex = 0;
|
||||||
sumDeltaAngle = 0;
|
sumDeltaAngle = 0;
|
||||||
for(int i=0; i<storedDeltaAngle.length; i++){
|
for (int i = 0; i < storedDeltaAngle.length; i++)
|
||||||
storedDeltaAngle[i] = 0;
|
storedDeltaAngle[i] = 0;
|
||||||
}
|
|
||||||
drawRotation = 0;
|
drawRotation = 0;
|
||||||
rotations = 0;
|
rotations = 0;
|
||||||
deltaOverflow = 0;
|
deltaOverflow = 0;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package itdelatrisu.opsu.render;
|
package itdelatrisu.opsu.render;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
import org.lwjgl.opengl.GL20;
|
import org.lwjgl.opengl.GL20;
|
||||||
import org.lwjgl.opengl.GL30;
|
import org.lwjgl.opengl.GL30;
|
||||||
|
|
|
@ -56,6 +56,9 @@ public class Replay {
|
||||||
/** The associated file. */
|
/** The associated file. */
|
||||||
private File file;
|
private File file;
|
||||||
|
|
||||||
|
/** The associated score data. */
|
||||||
|
private ScoreData scoreData;
|
||||||
|
|
||||||
/** Whether or not the replay data has been loaded from the file. */
|
/** Whether or not the replay data has been loaded from the file. */
|
||||||
public boolean loaded = false;
|
public boolean loaded = false;
|
||||||
|
|
||||||
|
@ -104,8 +107,6 @@ public class Replay {
|
||||||
/** Seed. (?) */
|
/** Seed. (?) */
|
||||||
public int seed;
|
public int seed;
|
||||||
|
|
||||||
private ScoreData scoreData;
|
|
||||||
|
|
||||||
/** Seed string. */
|
/** Seed string. */
|
||||||
private static final String SEED_STRING = "-12345";
|
private static final String SEED_STRING = "-12345";
|
||||||
|
|
||||||
|
@ -137,45 +138,15 @@ public class Replay {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the replay header only.
|
||||||
|
* @throws IOException failure to load the data
|
||||||
|
*/
|
||||||
public void loadHeader() throws IOException {
|
public void loadHeader() throws IOException {
|
||||||
OsuReader reader = new OsuReader(file);
|
OsuReader reader = new OsuReader(file);
|
||||||
loadHeader(reader);
|
loadHeader(reader);
|
||||||
reader.close();
|
reader.close();
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Returns a ScoreData object encapsulating all game data.
|
|
||||||
* If score data already exists, the existing object will be returned
|
|
||||||
* (i.e. this will not overwrite existing data).
|
|
||||||
* @param osu the OsuFile
|
|
||||||
* @return the ScoreData object
|
|
||||||
*/
|
|
||||||
public ScoreData getScoreData(Beatmap osu) {
|
|
||||||
if (scoreData != null)
|
|
||||||
return scoreData;
|
|
||||||
|
|
||||||
scoreData = new ScoreData();
|
|
||||||
scoreData.timestamp = file.lastModified() / 1000L;
|
|
||||||
scoreData.MID = osu.beatmapID;
|
|
||||||
scoreData.MSID = osu.beatmapSetID;
|
|
||||||
scoreData.title = osu.title;
|
|
||||||
scoreData.artist = osu.artist;
|
|
||||||
scoreData.creator = osu.creator;
|
|
||||||
scoreData.version = osu.version;
|
|
||||||
scoreData.hit300 = hit300;
|
|
||||||
scoreData.hit100 = hit100;
|
|
||||||
scoreData.hit50 = hit50;
|
|
||||||
scoreData.geki = geki;
|
|
||||||
scoreData.katu = katu;
|
|
||||||
scoreData.miss = miss;
|
|
||||||
scoreData.score = score;
|
|
||||||
scoreData.combo = combo;
|
|
||||||
scoreData.perfect = perfect;
|
|
||||||
scoreData.mods = mods;
|
|
||||||
scoreData.replayString = file!=null ? file.getName() : getReplayFilename();
|
|
||||||
scoreData.playerName = playerName!=null ? playerName : "No Name";
|
|
||||||
return scoreData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the replay header data.
|
* Loads the replay header data.
|
||||||
|
@ -260,6 +231,40 @@ public class Replay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a ScoreData object encapsulating all replay data.
|
||||||
|
* If score data already exists, the existing object will be returned
|
||||||
|
* (i.e. this will not overwrite existing data).
|
||||||
|
* @param beatmap the beatmap
|
||||||
|
* @return the ScoreData object
|
||||||
|
*/
|
||||||
|
public ScoreData getScoreData(Beatmap beatmap) {
|
||||||
|
if (scoreData != null)
|
||||||
|
return scoreData;
|
||||||
|
|
||||||
|
scoreData = new ScoreData();
|
||||||
|
scoreData.timestamp = file.lastModified() / 1000L;
|
||||||
|
scoreData.MID = beatmap.beatmapID;
|
||||||
|
scoreData.MSID = beatmap.beatmapSetID;
|
||||||
|
scoreData.title = beatmap.title;
|
||||||
|
scoreData.artist = beatmap.artist;
|
||||||
|
scoreData.creator = beatmap.creator;
|
||||||
|
scoreData.version = beatmap.version;
|
||||||
|
scoreData.hit300 = hit300;
|
||||||
|
scoreData.hit100 = hit100;
|
||||||
|
scoreData.hit50 = hit50;
|
||||||
|
scoreData.geki = geki;
|
||||||
|
scoreData.katu = katu;
|
||||||
|
scoreData.miss = miss;
|
||||||
|
scoreData.score = score;
|
||||||
|
scoreData.combo = combo;
|
||||||
|
scoreData.perfect = perfect;
|
||||||
|
scoreData.mods = mods;
|
||||||
|
scoreData.replayString = getReplayFilename();
|
||||||
|
scoreData.playerName = playerName;
|
||||||
|
return scoreData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the replay data to a file in the replays directory.
|
* Saves the replay data to a file in the replays directory.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,49 +1,119 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package itdelatrisu.opsu.replay;
|
package itdelatrisu.opsu.replay;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.ScoreData;
|
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.db.ScoreDB;
|
import itdelatrisu.opsu.db.ScoreDB;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.newdawn.slick.util.Log;
|
/**
|
||||||
|
* Importer for replay files.
|
||||||
|
*/
|
||||||
public class ReplayImporter {
|
public class ReplayImporter {
|
||||||
public static void importAllReplaysFromDir(File dir) {
|
/** The index of the current file being imported. */
|
||||||
for (File replayToImport : dir.listFiles()) {
|
private static int fileIndex = -1;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** The total number of replays to import. */
|
||||||
|
private static File[] files;
|
||||||
|
|
||||||
|
// This class should not be instantiated.
|
||||||
|
private ReplayImporter() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the importer for each OSR file in a directory, adding the replay
|
||||||
|
* to the score database and moving the file into the replay directory.
|
||||||
|
* @param dir the directory
|
||||||
|
*/
|
||||||
|
public static void importAllReplaysFromDir(File dir) {
|
||||||
|
// find all OSR files
|
||||||
|
files = dir.listFiles(new FilenameFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
return name.toLowerCase().endsWith(".osr");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (files == null || files.length < 1) {
|
||||||
|
files = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get replay directory
|
||||||
|
File replayDir = Options.getReplayDir();
|
||||||
|
if (!replayDir.isDirectory()) {
|
||||||
|
if (!replayDir.mkdir()) {
|
||||||
|
ErrorHandler.error(String.format("Failed to create replay directory '%s'.", replayDir.getAbsolutePath()), null, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// import OSRs
|
||||||
|
for (File file : files) {
|
||||||
|
fileIndex++;
|
||||||
|
Replay r = new Replay(file);
|
||||||
|
try {
|
||||||
|
r.loadHeader();
|
||||||
|
} catch (IOException e) {
|
||||||
|
ErrorHandler.error(String.format("Failed to import replay '%s'. The replay file could not be parsed.", file.getName()), e, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Beatmap beatmap = BeatmapSetList.get().getBeatmapFromHash(r.beatmapHash);
|
||||||
|
if (beatmap != null) {
|
||||||
|
File moveToFile = new File(replayDir, String.format("%s.osr", r.getReplayFilename()));
|
||||||
|
if (!file.renameTo(moveToFile)) {
|
||||||
|
ErrorHandler.error(String.format("Failed to import replay '%s'. The replay file could not be moved to the replay directory.", file.getName()), null, false);
|
||||||
|
//continue;
|
||||||
|
}
|
||||||
|
ScoreDB.addScore(r.getScoreData(beatmap));
|
||||||
|
} else {
|
||||||
|
ErrorHandler.error(String.format("Failed to import replay '%s'. The associated beatmap could not be found.", file.getName()), null, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileIndex = -1;
|
||||||
|
files = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the current file being imported, or null if none.
|
||||||
|
*/
|
||||||
|
public static String getCurrentFileName() {
|
||||||
|
if (files == null || fileIndex == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return files[fileIndex].getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the progress of replay importing, or -1 if not importing.
|
||||||
|
* @return the completion percent [0, 100] or -1
|
||||||
|
*/
|
||||||
|
public static int getLoadingProgress() {
|
||||||
|
if (files == null || fileIndex == -1)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return (fileIndex + 1) * 100 / files.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,9 @@ import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
|
||||||
import itdelatrisu.opsu.downloads.Download;
|
import itdelatrisu.opsu.downloads.Download;
|
||||||
import itdelatrisu.opsu.downloads.DownloadList;
|
import itdelatrisu.opsu.downloads.DownloadList;
|
||||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||||
|
|
|
@ -217,18 +217,15 @@ public class Game extends BasicGameState {
|
||||||
/** Playback speed (used in replays and "auto" mod). */
|
/** Playback speed (used in replays and "auto" mod). */
|
||||||
private PlaybackSpeed playbackSpeed;
|
private PlaybackSpeed playbackSpeed;
|
||||||
|
|
||||||
|
/** Whether the game is currently seeking to a replay position. */
|
||||||
|
private boolean isSeeking;
|
||||||
|
|
||||||
// game-related variables
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private int state;
|
||||||
|
|
||||||
private int width;
|
|
||||||
|
|
||||||
private int height;
|
|
||||||
|
|
||||||
private boolean seeking;
|
|
||||||
|
|
||||||
public Game(int state) {
|
public Game(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
@ -240,8 +237,8 @@ public class Game extends BasicGameState {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
input = container.getInput();
|
input = container.getInput();
|
||||||
|
|
||||||
width = container.getWidth();
|
int width = container.getWidth();
|
||||||
height = container.getHeight();
|
int height = container.getHeight();
|
||||||
|
|
||||||
// create offscreen graphics
|
// create offscreen graphics
|
||||||
offscreen = new Image(width, height);
|
offscreen = new Image(width, height);
|
||||||
|
@ -617,13 +614,14 @@ public class Game extends BasicGameState {
|
||||||
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
|
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
|
||||||
|
|
||||||
//TODO probably should to disable sounds then reseek to the new position
|
//TODO probably should to disable sounds then reseek to the new position
|
||||||
if(seeking && replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){
|
if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length &&
|
||||||
|
trackPosition < replay.frames[replayIndex - 1].getTime()) {
|
||||||
replayIndex = 0;
|
replayIndex = 0;
|
||||||
while(objectIndex>=0){
|
while (objectIndex >= 0) {
|
||||||
gameObjects[objectIndex].reset();
|
gameObjects[objectIndex].reset();
|
||||||
objectIndex--;
|
objectIndex--;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset game data
|
// reset game data
|
||||||
resetGameData();
|
resetGameData();
|
||||||
|
|
||||||
|
@ -635,7 +633,7 @@ public class Game extends BasicGameState {
|
||||||
timingPointIndex++;
|
timingPointIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seeking = false;
|
isSeeking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update and run replay frames
|
// update and run replay frames
|
||||||
|
@ -925,10 +923,11 @@ public class Game extends BasicGameState {
|
||||||
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!GameMod.AUTO.isActive() && y < 50){
|
// TODO
|
||||||
float pos = (float)x / width * beatmap.endTime;
|
else if (!GameMod.AUTO.isActive() && y < 50) {
|
||||||
MusicController.setPosition((int)pos);
|
float pos = (float) x / container.getWidth() * beatmap.endTime;
|
||||||
seeking = true;
|
MusicController.setPosition((int) pos);
|
||||||
|
isSeeking = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1439,19 +1438,16 @@ public class Game extends BasicGameState {
|
||||||
|
|
||||||
// overallDifficulty (hit result time offsets)
|
// overallDifficulty (hit result time offsets)
|
||||||
hitResultOffset = new int[GameData.HIT_MAX];
|
hitResultOffset = new int[GameData.HIT_MAX];
|
||||||
/*
|
|
||||||
float mult = 0.608f;
|
|
||||||
hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6))*mult);
|
|
||||||
hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8))*mult);
|
|
||||||
hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16))*mult);
|
|
||||||
hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10))*mult);
|
|
||||||
/*/
|
|
||||||
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
||||||
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
||||||
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
||||||
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
||||||
|
//final float mult = 0.608f;
|
||||||
|
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
|
||||||
|
//hitResultOffset[GameData.HIT_100] = (int) ((224 - (overallDifficulty * 12.8)) * mult);
|
||||||
|
//hitResultOffset[GameData.HIT_50] = (int) ((320 - (overallDifficulty * 16)) * mult);
|
||||||
|
//hitResultOffset[GameData.HIT_MISS] = (int) ((1000 - (overallDifficulty * 10)) * mult);
|
||||||
data.setHitResultOffset(hitResultOffset);
|
data.setHitResultOffset(hitResultOffset);
|
||||||
//*/
|
|
||||||
|
|
||||||
// HPDrainRate (health change)
|
// HPDrainRate (health change)
|
||||||
data.setDrainRate(HPDrainRate);
|
data.setDrainRate(HPDrainRate);
|
||||||
|
|
|
@ -32,8 +32,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
import itdelatrisu.opsu.downloads.Updater;
|
import itdelatrisu.opsu.downloads.Updater;
|
||||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
|
||||||
import itdelatrisu.opsu.ui.MenuButton.Expand;
|
import itdelatrisu.opsu.ui.MenuButton.Expand;
|
||||||
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -32,10 +32,10 @@ import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
|
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
|
||||||
import itdelatrisu.opsu.db.BeatmapDB;
|
import itdelatrisu.opsu.db.BeatmapDB;
|
||||||
import itdelatrisu.opsu.db.ScoreDB;
|
import itdelatrisu.opsu.db.ScoreDB;
|
||||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||||
|
|
|
@ -25,9 +25,9 @@ import itdelatrisu.opsu.OszUnpacker;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.replay.ReplayImporter;
|
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
|
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import itdelatrisu.opsu.OszUnpacker;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
|
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
|
@ -285,6 +286,9 @@ public class UI {
|
||||||
text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ?
|
text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ?
|
||||||
"Updating database..." : "Loading beatmaps...";
|
"Updating database..." : "Loading beatmaps...";
|
||||||
progress = BeatmapParser.getParserProgress();
|
progress = BeatmapParser.getParserProgress();
|
||||||
|
} else if ((file = ReplayImporter.getCurrentFileName()) != null) {
|
||||||
|
text = "Importing replays...";
|
||||||
|
progress = ReplayImporter.getLoadingProgress();
|
||||||
} else if ((file = SoundController.getCurrentFileName()) != null) {
|
} else if ((file = SoundController.getCurrentFileName()) != null) {
|
||||||
text = "Loading sounds...";
|
text = "Loading sounds...";
|
||||||
progress = SoundController.getLoadingProgress();
|
progress = SoundController.getLoadingProgress();
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.lwjgl.openal.OpenALException;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
import org.newdawn.slick.util.ResourceLoader;
|
import org.newdawn.slick.util.ResourceLoader;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL
|
* A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL
|
||||||
* as required.
|
* as required.
|
||||||
|
@ -356,7 +355,7 @@ public class OpenALStreamPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
playedPos = streamPos;
|
playedPos = streamPos;
|
||||||
syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate)/pitch);
|
syncStartTime = (long) (getTime() - (playedPos * 1000 / sampleSize / sampleRate) / pitch);
|
||||||
|
|
||||||
startPlayback();
|
startPlayback();
|
||||||
|
|
||||||
|
@ -409,7 +408,6 @@ public class OpenALStreamPlayer {
|
||||||
|
|
||||||
// hard reset
|
// hard reset
|
||||||
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
|
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
|
||||||
//System.out.println("Time HARD Reset"+" "+thisPosition+" "+(dxTime / 1000f));
|
|
||||||
syncPosition();
|
syncPosition();
|
||||||
dxTime = (thisTime - syncStartTime) * pitch;
|
dxTime = (thisTime - syncStartTime) * pitch;
|
||||||
avgDiff = 0;
|
avgDiff = 0;
|
||||||
|
@ -418,19 +416,22 @@ public class OpenALStreamPlayer {
|
||||||
float diff = thisPosition * 1000 - (dxTime);
|
float diff = thisPosition * 1000 - (dxTime);
|
||||||
|
|
||||||
avgDiff = (diff + avgDiff * 9) / 10;
|
avgDiff = (diff + avgDiff * 9) / 10;
|
||||||
if(Math.abs(avgDiff) >= 1){
|
if (Math.abs(avgDiff) >= 1) {
|
||||||
syncStartTime -= (int)(avgDiff);
|
syncStartTime -= (int) (avgDiff);
|
||||||
avgDiff -= (int)(avgDiff);
|
avgDiff -= (int) (avgDiff);
|
||||||
dxTime = (thisTime - syncStartTime) * pitch;
|
dxTime = (thisTime - syncStartTime) * pitch;
|
||||||
}
|
}
|
||||||
lastUpdatePosition = thisPosition;
|
lastUpdatePosition = thisPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return dxTime / 1000f;
|
return dxTime / 1000f;
|
||||||
}
|
}
|
||||||
private void syncPosition(){
|
|
||||||
syncStartTime = getTime() - (long) ( getALPosition() * 1000 / pitch);
|
/**
|
||||||
|
* Synchronizes the track position.
|
||||||
|
*/
|
||||||
|
private void syncPosition() {
|
||||||
|
syncStartTime = getTime() - (long) (getALPosition() * 1000 / pitch);
|
||||||
avgDiff = 0;
|
avgDiff = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user