iter = hitResultList.iterator();
while (iter.hasNext()) {
- OsuHitObjectResult hitResult = iter.next();
- if (hitResult.time + HITRESULT_FADE_TIME > trackPosition) {
- // hit result
- hitResults[hitResult.result].setAlpha(hitResult.alpha);
- hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y);
- hitResults[hitResult.result].setAlpha(1f);
-
+ HitObjectResult hitResult = iter.next();
+ if (hitResult.time + HITRESULT_TIME > trackPosition) {
// spinner
- if (hitResult.isSpinner && hitResult.result != HIT_MISS) {
+ if (hitResult.hitResultType == HitObjectType.SPINNER && hitResult.result != HIT_MISS) {
Image spinnerOsu = GameImage.SPINNER_OSU.getImage();
spinnerOsu.setAlpha(hitResult.alpha);
spinnerOsu.drawCentered(width / 2, height / 4);
@@ -859,26 +907,75 @@ public class GameData {
// hit lighting
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
- float scale = 1f + ((trackPosition - hitResult.time) / (float) HITRESULT_FADE_TIME);
- Image scaledLighting = GameImage.LIGHTING.getImage().getScaledCopy(scale);
- Image scaledLighting1 = GameImage.LIGHTING1.getImage().getScaledCopy(scale);
- scaledLighting.setAlpha(hitResult.alpha);
- scaledLighting1.setAlpha(hitResult.alpha);
+ // TODO: add particle system
+ Image lighting = GameImage.LIGHTING.getImage();
+ lighting.setAlpha(hitResult.alpha);
+ lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
+ }
- scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f),
- hitResult.y - (scaledLighting.getHeight() / 2f), hitResult.color);
- scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f),
- hitResult.y - (scaledLighting1.getHeight() / 2f), hitResult.color);
+ // hit animation
+ if (hitResult.result != HIT_MISS && (
+ hitResult.hitResultType == HitObjectType.CIRCLE ||
+ hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
+ hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
+ float scale = (!hitResult.expand) ? 1f : Utils.easeOut(
+ Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
+ 1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME
+ );
+ float alpha = Utils.easeOut(
+ Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
+ 1f, -1f, HITCIRCLE_FADE_TIME
+ );
+
+ // slider curve
+ if (hitResult.curve != null) {
+ float oldWhiteAlpha = Utils.COLOR_WHITE_FADE.a;
+ float oldColorAlpha = hitResult.color.a;
+ Utils.COLOR_WHITE_FADE.a = alpha;
+ hitResult.color.a = alpha;
+ hitResult.curve.draw(hitResult.color);
+ Utils.COLOR_WHITE_FADE.a = oldWhiteAlpha;
+ hitResult.color.a = oldColorAlpha;
+ }
+
+ // hit circles
+ Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
+ Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
+ scaledHitCircle.setAlpha(alpha);
+ scaledHitCircleOverlay.setAlpha(alpha);
+ scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
+ scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
+ }
+
+ // hit result
+ if (hitResult.hitResultType == HitObjectType.CIRCLE ||
+ hitResult.hitResultType == HitObjectType.SPINNER ||
+ hitResult.curve != null) {
+ float scale = Utils.easeBounce(
+ Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME),
+ 1f, HITCIRCLE_TEXT_ANIM_SCALE - 1f, HITCIRCLE_TEXT_BOUNCE_TIME
+ );
+ float alpha = Utils.easeOut(
+ Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME),
+ 1f, -1f, HITCIRCLE_TEXT_FADE_TIME
+ );
+ Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
+ scaledHitResult.setAlpha(alpha);
+ scaledHitResult.drawCentered(hitResult.x, hitResult.y);
}
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
- } else
+ } else {
+ if (hitResult.curve != null)
+ hitResult.curve.discardCache();
iter.remove();
+ }
}
}
/**
* Changes health by a given percentage, modified by drainRate.
+ * @param percent the health percentage
*/
public void changeHealth(float percent) {
// TODO: drainRate formula
@@ -890,7 +987,7 @@ public class GameData {
}
/**
- * Returns health percentage.
+ * Returns the current health percentage.
*/
public float getHealth() { return health; }
@@ -905,6 +1002,7 @@ public class GameData {
/**
* Changes score by a raw value (not affected by other modifiers).
+ * @param value the score value
*/
public void changeScore(int value) { score += value; }
@@ -914,7 +1012,7 @@ public class GameData {
* @param hit100 the number of 100s
* @param hit50 the number of 50s
* @param miss the number of misses
- * @return the percentage
+ * @return the score percentage
*/
public static float getScorePercent(int hit300, int hit100, int hit50, int miss) {
float percent = 0;
@@ -969,7 +1067,7 @@ public class GameData {
/**
* Returns letter grade based on score data,
- * or Grade.NULL if no objects have been processed.
+ * or {@code Grade.NULL} if no objects have been processed.
*/
private Grade getGrade() {
return getGrade(
@@ -1073,10 +1171,14 @@ public class GameData {
// combo bursts (at 30, 60, 100+50x)
if (Options.isComboBurstEnabled() &&
(combo == 30 || combo == 60 || (combo >= 100 && combo % 50 == 0))) {
- if (combo == 30)
- comboBurstIndex = 0;
- else
- comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
+ if (Options.getSkin().isComboBurstRandom())
+ comboBurstIndex = (int) (Math.random() * comboBurstImages.length);
+ else {
+ if (combo == 30)
+ comboBurstIndex = 0;
+ else
+ comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
+ }
comboBurstAlpha = 0.8f;
if ((comboBurstIndex % 2) == 0)
comboBurstX = width;
@@ -1105,7 +1207,7 @@ public class GameData {
* @param hitObject the hit object
* @param repeat the current repeat number
*/
- public void sliderTickResult(int time, int result, float x, float y, OsuHitObject hitObject, int repeat) {
+ public void sliderTickResult(int time, int result, float x, float y, HitObject hitObject, int repeat) {
int hitValue = 0;
switch (result) {
case HIT_SLIDER30:
@@ -1135,7 +1237,7 @@ public class GameData {
if (!Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else
- hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, false));
+ hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false));
}
}
public void sliderFinalResult(int time, int hitSlider30, float x, float y,
@@ -1144,23 +1246,41 @@ public class GameData {
}
/**
- * Handles a hit result.
+ * Returns the score for a hit based on the following score formula:
+ *
+ * Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
+ *
+ * - Hit Value: hit result (50, 100, 300), slider ticks, spinner bonus
+ *
- Combo: combo before this hit - 1 (minimum 0)
+ *
- Difficulty: the beatmap difficulty
+ *
- Mod: mod multipliers
+ *
+ * @param hitValue the hit value
+ * @return the score value
+ */
+ private int getScoreForHit(int hitValue) {
+ return hitValue + (int) (hitValue * (Math.max(combo - 1, 0) * difficulty * GameMod.getScoreMultiplier()) / 25);
+ }
+
+ /**
+ * Handles a hit result and performs all associated calculations.
* @param time the object start time
- * @param result the hit result (HIT_* constants)
+ * @param result the base hit result (HIT_* constants)
* @param x the x coordinate
* @param y the y coordinate
* @param color the combo color
* @param end true if this is the last hit object in the combo
* @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
+ * @return the actual hit result (HIT_* constants)
*/
- public void hitResult(int time, int result, float x, float y, Color color,
- boolean end, OsuHitObject hitObject, int repeat) {
+ private int handleHitResult(int time, int result, float x, float y, Color color,
+ boolean end, HitObject hitObject, int repeat, HitObjectType hitResultType) {
+ // update health, score, and combo streak based on hit result
int hitValue = 0;
- boolean perfectHit = false;
switch (result) {
case HIT_300:
- perfectHit = true;
hitValue = 300;
changeHealth(5f);
break;
@@ -1183,13 +1303,15 @@ public class GameData {
resetComboStreak();
break;
default:
- return;
+ return HIT_MISS;
}
if (hitValue > 0) {
SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat),
hitObject.getAdditionSampleSet(repeat));
+ //TODO merge conflict
+
/**
* https://osu.ppy.sh/wiki/Score
* [SCORE FORMULA]
@@ -1204,7 +1326,11 @@ public class GameData {
comboMulti += 1;
}
score += (hitValue + (hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25));
+
+ // calculate score and increment combo streak
+ changeScore(getScoreForHit(hitValue));
incrementComboStreak();
+ //merge conflict end
}
hitResultCount[result]++;
fullObjectCount++;
@@ -1229,12 +1355,43 @@ public class GameData {
comboEnd = 0;
}
- if (perfectHit && !Options.isPerfectHitBurstEnabled())
+ return result;
+ }
+
+ /**
+ * Handles a slider hit result.
+ * @param time the object start time
+ * @param result the hit result (HIT_* constants)
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param color the combo color
+ * @param end true if this is the last hit object in the combo
+ * @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 curve the slider curve (or null if not applicable)
+ * @param expand whether or not the hit result animation should expand (if applicable)
+ */
+ public void hitResult(int time, int result, float x, float y, Color color,
+ boolean end, HitObject hitObject, int repeat,
+ HitObjectType hitResultType, Curve curve, boolean expand) {
+ result = handleHitResult(time, result, x, y, color, end, hitObject, repeat, hitResultType);
+
+ if ((result == HIT_300 || result == HIT_300G || result == HIT_300K) && !Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
; // "relax" and "autopilot" mods: hide misses
- else
- hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitObject.isSpinner()));
+ else {
+ hitResultList.add(new HitObjectResult(time, result, x, y, color, hitResultType, curve, expand));
+
+ // sliders: add the other curve endpoint for the hit animation
+ if (curve != null) {
+ boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
+ float[] p = curve.pointAt((isFirst) ? 1f : 0f);
+ HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
+ hitResultList.add(new HitObjectResult(time, result, p[0], p[1], color, type, null, expand));
+ }
+ }
}
private int getDifficultyMultiplier() {
@@ -1258,21 +1415,21 @@ public class GameData {
* 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
+ * @param beatmap the beatmap
* @return the ScoreData object
*/
- public ScoreData getScoreData(OsuFile osu) {
+ public ScoreData getScoreData(Beatmap beatmap) {
if (scoreData != null)
return scoreData;
scoreData = new ScoreData();
scoreData.timestamp = System.currentTimeMillis() / 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.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 = hitResultCount[HIT_300];
scoreData.hit100 = hitResultCount[HIT_100];
scoreData.hit50 = hitResultCount[HIT_50];
@@ -1292,10 +1449,10 @@ public class GameData {
* Returns a Replay object encapsulating all game data.
* If a replay already exists and frames is null, the existing object will be returned.
* @param frames the replay frames
- * @param osu the associated OsuFile
+ * @param beatmap the associated beatmap
* @return the Replay object, or null if none exists and frames is null
*/
- public Replay getReplay(ReplayFrame[] frames, OsuFile osu) {
+ public Replay getReplay(ReplayFrame[] frames, Beatmap beatmap) {
if (replay != null && frames == null)
return replay;
@@ -1303,9 +1460,9 @@ public class GameData {
return null;
replay = new Replay();
- replay.mode = OsuFile.MODE_OSU;
+ replay.mode = Beatmap.MODE_OSU;
replay.version = Updater.get().getBuildDate();
- replay.beatmapHash = (osu == null) ? "" : Utils.getMD5(osu.getFile());
+ replay.beatmapHash = (beatmap == null) ? "" : Utils.getMD5(beatmap.getFile());
replay.playerName = ""; // TODO
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300];
@@ -1329,6 +1486,7 @@ public class GameData {
/**
* Sets the replay object.
+ * @param replay the replay
*/
public void setReplay(Replay replay) { this.replay = replay; }
diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java
index 12f9a814..c8cb3bfb 100644
--- a/src/itdelatrisu/opsu/GameImage.java
+++ b/src/itdelatrisu/opsu/GameImage.java
@@ -110,6 +110,7 @@ public enum GameImage {
APPROACHCIRCLE ("approachcircle", "png"),
// Slider
+ SLIDER_GRADIENT ("slidergradient", "png"),
SLIDER_BALL ("sliderb", "sliderb%d", "png"),
SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"),
REVERSEARROW ("reversearrow", "png"),
@@ -202,7 +203,6 @@ public enum GameImage {
SCORE_PERCENT ("score-percent", "png"),
SCORE_X ("score-x", "png"),
LIGHTING ("lighting", "png"),
- LIGHTING1 ("lighting1", "png"),
// Game Mods
MOD_EASY ("selection-mod-easy", "png", false, false),
@@ -228,6 +228,11 @@ public enum GameImage {
SELECTION_OTHER_OPTIONS ("selection-selectoptions", "png", false, false),
SELECTION_OTHER_OPTIONS_OVERLAY ("selection-selectoptions-over", "png", false, false),
+ // Replay Speed Buttons
+ REPLAY_PLAYBACK_NORMAL ("playback-normal", "png", false, false),
+ REPLAY_PLAYBACK_DOUBLE ("playback-double", "png", false, false),
+ REPLAY_PLAYBACK_HALF ("playback-half", "png", false, false),
+
// Non-Game Components
VOLUME ("volume-bg", "png", false, false) {
@Override
@@ -329,6 +334,14 @@ public enum GameImage {
return REPOSITORY.process_sub(img, w, h);
}
},
+ OPTIONS_BG ("options-background", "png|jpg", false, true) {
+ @Override
+ protected Image process_sub(Image img, int w, int h) {
+ img.setAlpha(0.7f);
+ return img.getScaledCopy(w, h);
+ }
+ },
+
// TODO: ensure this image hasn't been modified (checksum?)
ALPHA_MAP ("alpha", "png", false, false);
@@ -376,10 +389,13 @@ public enum GameImage {
/** The unscaled container height that uiscale is based on. */
private static final int UNSCALED_HEIGHT = 768;
+ /** Filename suffix for HD images. */
+ public static final String HD_SUFFIX = "@2x";
+
/** Image HD/SD suffixes. */
private static final String[]
- SUFFIXES_HD = new String[] { "@2x", "" },
- SUFFIXES_SD = new String[] { "" };
+ SUFFIXES_HD = new String[] { HD_SUFFIX, "" },
+ SUFFIXES_SD = new String[] { "" };
/**
* Initializes the GameImage class with container dimensions.
@@ -550,6 +566,7 @@ public enum GameImage {
/**
* Sets the image associated with this resource to another image.
* The skin image takes priority over the default image.
+ * @param img the image to set
*/
public void setImage(Image img) {
if (skinImage != null)
@@ -561,6 +578,8 @@ public enum GameImage {
/**
* Sets an image associated with this resource to another image.
* The skin image takes priority over the default image.
+ * @param img the image to set
+ * @param index the index in the image array
*/
public void setImage(Image img, int index) {
if (skinImages != null) {
@@ -581,8 +600,9 @@ public enum GameImage {
return;
// try to load multiple images
+ File skinDir = Options.getSkin().getDirectory();
if (filenameFormat != null) {
- if (((defaultImages = loadImageArray(Options.getSkinDir())) != null) ||
+ if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
((defaultImages = loadImageArray(null)) != null)) {
process();
return;
@@ -590,7 +610,7 @@ public enum GameImage {
}
// try to load a single image
- if (((defaultImage = loadImageSingle(Options.getSkinDir())) != null) ||
+ if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
((defaultImage = loadImageSingle(null)) != null)) {
process();
return;
@@ -602,6 +622,7 @@ public enum GameImage {
/**
* Sets the associated skin image.
* If the path does not contain the image, the default image is used.
+ * @param dir the image directory to search
* @return true if a new skin image is loaded, false otherwise
*/
public boolean setSkinImage(File dir) {
@@ -632,6 +653,7 @@ public enum GameImage {
/**
* Attempts to load multiple Images from the GameImage.
+ * @param dir the image directory to search, or null to use the default resource locations
* @return an array of the loaded images, or null if not found
*/
private Image[] loadImageArray(File dir) {
@@ -649,7 +671,7 @@ public enum GameImage {
// add image to list
try {
Image img = new Image(name);
- if (suffix.equals("@2x"))
+ if (suffix.equals(HD_SUFFIX))
img = img.getScaledCopy(0.5f);
list.add(img);
} catch (SlickException e) {
@@ -666,6 +688,7 @@ public enum GameImage {
/**
* Attempts to load a single Image from the GameImage.
+ * @param dir the image directory to search, or null to use the default resource locations
* @return the loaded image, or null if not found
*/
private Image loadImageSingle(File dir) {
@@ -674,7 +697,7 @@ public enum GameImage {
if (name != null) {
try {
Image img = new Image(name);
- if (suffix.equals("@2x"))
+ if (suffix.equals(HD_SUFFIX))
img = img.getScaledCopy(0.5f);
return img;
} catch (SlickException e) {
diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java
index a7bd3e97..ff7c86a0 100644
--- a/src/itdelatrisu/opsu/GameMod.java
+++ b/src/itdelatrisu/opsu/GameMod.java
@@ -18,6 +18,8 @@
package itdelatrisu.opsu;
+import itdelatrisu.opsu.ui.MenuButton;
+
import java.util.Arrays;
import java.util.Collections;
@@ -33,7 +35,7 @@ public enum GameMod {
"Easy", "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required."),
NO_FAIL (Category.EASY, 1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f,
"NoFail", "You can't fail. No matter what."),
- HALF_TIME (Category.EASY, 2, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f, false,
+ HALF_TIME (Category.EASY, 2, GameImage.MOD_HALF_TIME, "HT", 256, Input.KEY_E, 0.3f,
"HalfTime", "Less zoom."),
HARD_ROCK (Category.HARD, 0, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f,
"HardRock", "Everything just got a bit harder..."),
@@ -41,7 +43,7 @@ public enum GameMod {
"SuddenDeath", "Miss a note and fail."),
// PERFECT (Category.HARD, 1, GameImage.MOD_PERFECT, "PF", 64, Input.KEY_S, 1f,
// "Perfect", "SS or quit."),
- DOUBLE_TIME (Category.HARD, 2, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f, false,
+ DOUBLE_TIME (Category.HARD, 2, GameImage.MOD_DOUBLE_TIME, "DT", 64, Input.KEY_D, 1.12f,
"DoubleTime", "Zoooooooooom."),
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
// "Nightcore", "uguuuuuuuu"),
@@ -173,6 +175,12 @@ public enum GameMod {
/** The last calculated score multiplier, or -1f if it must be recalculated. */
private static float scoreMultiplier = -1f;
+ /** The last calculated track speed multiplier, or -1f if it must be recalculated. */
+ private static float speedMultiplier = -1f;
+
+ /** The last calculated difficulty multiplier, or -1f if it must be recalculated. */
+ private static float difficultyMultiplier = -1f;
+
/**
* Initializes the game mods.
* @param width the container width
@@ -198,7 +206,7 @@ public enum GameMod {
mod.active = false;
}
- scoreMultiplier = -1f;
+ scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
}
/**
@@ -216,6 +224,36 @@ public enum GameMod {
return scoreMultiplier;
}
+ /**
+ * Returns the current track speed multiplier from all active mods.
+ */
+ public static float getSpeedMultiplier() {
+ if (speedMultiplier < 0f) {
+ if (DOUBLE_TIME.isActive())
+ speedMultiplier = 1.5f;
+ else if (HALF_TIME.isActive())
+ speedMultiplier = 0.75f;
+ else
+ speedMultiplier = 1f;
+ }
+ return speedMultiplier;
+ }
+
+ /**
+ * Returns the current difficulty multiplier from all active mods.
+ */
+ public static float getDifficultyMultiplier() {
+ if (difficultyMultiplier < 0f) {
+ if (HARD_ROCK.isActive())
+ difficultyMultiplier = 1.4f;
+ else if (EASY.isActive())
+ difficultyMultiplier = 0.5f;
+ else
+ difficultyMultiplier = 1f;
+ }
+ return difficultyMultiplier;
+ }
+
/**
* Returns the current game mod state (bitwise OR of active mods).
*/
@@ -233,6 +271,7 @@ public enum GameMod {
* @param state the state (bitwise OR of active mods)
*/
public static void loadModState(int state) {
+ scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
for (GameMod mod : GameMod.values())
mod.active = ((state & mod.getBit()) > 0);
}
@@ -352,7 +391,7 @@ public enum GameMod {
return;
active = !active;
- scoreMultiplier = -1f;
+ scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
if (checkInverse) {
if (AUTO.isActive()) {
diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java
index 078e791d..1f89dd5b 100644
--- a/src/itdelatrisu/opsu/Opsu.java
+++ b/src/itdelatrisu/opsu/Opsu.java
@@ -31,6 +31,7 @@ import itdelatrisu.opsu.states.MainMenu;
import itdelatrisu.opsu.states.OptionsMenu;
import itdelatrisu.opsu.states.SongMenu;
import itdelatrisu.opsu.states.Splash;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.io.FileNotFoundException;
@@ -45,7 +46,6 @@ import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.FadeOutTransition;
-import org.newdawn.slick.util.ClasspathLocation;
import org.newdawn.slick.util.DefaultLogSystem;
import org.newdawn.slick.util.FileSystemLocation;
import org.newdawn.slick.util.Log;
@@ -128,14 +128,14 @@ public class Opsu extends StateBasedGame {
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
// set the resource paths
- ResourceLoader.removeAllResourceLocations();
- ResourceLoader.addResourceLocation(new FileSystemLocation(Options.getSkinDir()));
- ResourceLoader.addResourceLocation(new ClasspathLocation());
- ResourceLoader.addResourceLocation(new FileSystemLocation(new File(".")));
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
// initialize databases
- DBController.init();
+ try {
+ DBController.init();
+ } catch (UnsatisfiedLinkError e) {
+ errorAndExit(e, "The databases could not be initialized.");
+ }
// check if just updated
if (args.length >= 2)
@@ -176,12 +176,7 @@ public class Opsu extends StateBasedGame {
}
}
} catch (SlickException e) {
- // JARs will not run properly inside directories containing '!'
- // http://bugs.java.com/view_bug.do?bug_id=4523159
- if (new File("").getAbsolutePath().indexOf('!') != -1)
- ErrorHandler.error("Cannot run JAR from path containing '!'.", null, false);
- else
- ErrorHandler.error("Error while creating game container.", e, true);
+ errorAndExit(e, "An error occurred while creating the game container.");
}
}
@@ -207,7 +202,8 @@ public class Opsu extends StateBasedGame {
} else
songMenu.resetTrackOnLoad();
}
- UI.resetCursor();
+ if (UI.getCursor().isSkinned())
+ UI.getCursor().reset();
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return false;
}
@@ -242,4 +238,20 @@ public class Opsu extends StateBasedGame {
}
}
}
+
+ /**
+ * Throws an error and exits the application with the given message.
+ * @param e the exception that caused the crash
+ * @param message the message to display
+ */
+ private static void errorAndExit(Throwable e, String message) {
+ // JARs will not run properly inside directories containing '!'
+ // http://bugs.java.com/view_bug.do?bug_id=4523159
+ if (Utils.isJarRunning() && Utils.getRunningDirectory() != null &&
+ Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1)
+ ErrorHandler.error("JARs cannot be run from some paths containing '!'. Please move or rename the file and try again.", null, false);
+ else
+ ErrorHandler.error(message, e, true);
+ System.exit(1);
+ }
}
diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java
index 00a0f901..ecfe30b9 100644
--- a/src/itdelatrisu/opsu/Options.java
+++ b/src/itdelatrisu/opsu/Options.java
@@ -19,6 +19,10 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.skins.Skin;
+import itdelatrisu.opsu.skins.SkinLoader;
+import itdelatrisu.opsu.ui.UI;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@@ -30,6 +34,7 @@ import java.io.OutputStreamWriter;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@@ -37,7 +42,10 @@ import org.lwjgl.input.Keyboard;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
+import org.newdawn.slick.util.ClasspathLocation;
+import org.newdawn.slick.util.FileSystemLocation;
import org.newdawn.slick.util.Log;
+import org.newdawn.slick.util.ResourceLoader;
/**
* Handles all user options.
@@ -55,21 +63,28 @@ public class Options {
/** File for storing user options. */
private static final File OPTIONS_FILE = new File(CONFIG_DIR, ".opsu.cfg");
- /** Beatmap directories (where to search for files). */
+ /** Beatmap directories (where to search for files). */
private static final String[] BEATMAP_DIRS = {
"C:/Program Files (x86)/osu!/Songs/",
"C:/Program Files/osu!/Songs/",
new File(DATA_DIR, "Songs/").getPath()
};
+ /** Skin directories (where to search for skins). */
+ private static final String[] SKIN_ROOT_DIRS = {
+ "C:/Program Files (x86)/osu!/Skins/",
+ "C:/Program Files/osu!/Skins/",
+ new File(DATA_DIR, "Skins/").getPath()
+ };
+
/** Cached beatmap database name. */
- public static final File OSU_DB = new File(DATA_DIR, ".opsu.db");
+ public static final File BEATMAP_DB = new File(DATA_DIR, ".opsu.db");
/** Score database name. */
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
/** Font file name. */
- public static final String FONT_NAME = "kochi-gothic.ttf";
+ public static final String FONT_NAME = "DroidSansFallback.ttf";
/** Version file name. */
public static final String VERSION_FILE = "version";
@@ -98,8 +113,8 @@ public class Options {
/** The replay import directory. */
private static File replayImportDir;
- /** The current skin directory (for user skins). */
- private static File skinDir;
+ /** The root skin directory. */
+ private static File skinRootDir;
/** Port binding. */
private static int port = 49250;
@@ -140,8 +155,63 @@ public class Options {
/** Game options. */
public enum GameOption {
- NULL (null, null),
- SCREEN_RESOLUTION ("Screen Resolution", "Restart (Ctrl+Shift+F5) to apply resolution changes.") {
+ // internal options (not displayed in-game)
+ BEATMAP_DIRECTORY ("BeatmapDirectory") {
+ @Override
+ public String write() { return getBeatmapDir().getAbsolutePath(); }
+
+ @Override
+ public void read(String s) { beatmapDir = new File(s); }
+ },
+ OSZ_DIRECTORY ("OSZDirectory") {
+ @Override
+ public String write() { return getOSZDir().getAbsolutePath(); }
+
+ @Override
+ public void read(String s) { oszDir = new File(s); }
+ },
+ SCREENSHOT_DIRECTORY ("ScreenshotDirectory") {
+ @Override
+ public String write() { return getScreenshotDir().getAbsolutePath(); }
+
+ @Override
+ public void read(String s) { screenshotDir = new File(s); }
+ },
+ REPLAY_DIRECTORY ("ReplayDirectory") {
+ @Override
+ public String write() { return getReplayDir().getAbsolutePath(); }
+
+ @Override
+ public void read(String s) { replayDir = new File(s); }
+ },
+ SKIN_DIRECTORY ("SkinDirectory") {
+ @Override
+ public String write() { return getSkinRootDir().getAbsolutePath(); }
+
+ @Override
+ public void read(String s) { skinRootDir = new File(s); }
+ },
+ THEME_SONG ("ThemeSong") {
+ @Override
+ public String write() { return themeString; }
+
+ @Override
+ public void read(String s) { themeString = s; }
+ },
+ PORT ("Port") {
+ @Override
+ public String write() { return Integer.toString(port); }
+
+ @Override
+ public void read(String s) {
+ int i = Integer.parseInt(s);
+ if (i > 0 && i < 65535)
+ port = i;
+ }
+ },
+
+ // in-game options
+ SCREEN_RESOLUTION ("Screen Resolution", "ScreenResolution", "Restart (Ctrl+Shift+F5) to apply resolution changes.") {
@Override
public String getValueString() { return resolution.toString(); }
@@ -149,13 +219,34 @@ public class Options {
public void click(GameContainer container) {
do {
resolution = resolution.next();
- } while (resolution != Resolution.RES_800_600 &&
- (container.getScreenWidth() < resolution.getWidth() ||
- container.getScreenHeight() < resolution.getHeight()));
+ } while (resolution != Resolution.RES_800_600 && (
+ container.getScreenWidth() < resolution.getWidth() ||
+ container.getScreenHeight() < resolution.getHeight()));
+ }
+
+ @Override
+ public void read(String s) {
+ try {
+ Resolution res = Resolution.valueOf(String.format("RES_%s", s.replace('x', '_')));
+ resolution = res;
+ } catch (IllegalArgumentException e) {}
}
},
-// FULLSCREEN ("Fullscreen Mode", "Restart to apply changes.", false),
- TARGET_FPS ("Frame Limiter", "Higher values may cause high CPU usage.") {
+// FULLSCREEN ("Fullscreen Mode", "Fullscreen", "Restart to apply changes.", false),
+ SKIN ("Skin", "Skin", "Restart (Ctrl+Shift+F5) to apply skin changes.") {
+ @Override
+ public String getValueString() { return skinName; }
+
+ @Override
+ public void click(GameContainer container) {
+ skinDirIndex = (skinDirIndex + 1) % skinDirs.length;
+ skinName = skinDirs[skinDirIndex];
+ }
+
+ @Override
+ public void read(String s) { skinName = s; }
+ },
+ TARGET_FPS ("Frame Limiter", "FrameSync", "Higher values may cause high CPU usage.") {
@Override
public String getValueString() {
return String.format((getTargetFPS() == 60) ? "%dfps (vsync)" : "%dfps", getTargetFPS());
@@ -167,86 +258,23 @@ public class Options {
container.setTargetFrameRate(getTargetFPS());
container.setVSync(getTargetFPS() == 60);
}
- },
- MASTER_VOLUME ("Master Volume", "Global volume level.", 35, 0, 100) {
- @Override
- public void drag(GameContainer container, int d) {
- super.drag(container, d);
- container.setMusicVolume(getMasterVolume() * getMusicVolume());
- }
- },
- MUSIC_VOLUME ("Music Volume", "Volume of music.", 80, 0, 100) {
- @Override
- public void drag(GameContainer container, int d) {
- super.drag(container, d);
- container.setMusicVolume(getMasterVolume() * getMusicVolume());
- }
- },
- EFFECT_VOLUME ("Effect Volume", "Volume of menu and game sounds.", 70, 0, 100),
- HITSOUND_VOLUME ("Hit Sound Volume", "Volume of hit sounds.", 30, 0, 100),
- MUSIC_OFFSET ("Music Offset", "Adjust this value if hit objects are out of sync.", -75, -500, 500) {
- @Override
- public String getValueString() { return String.format("%dms", val); }
- },
- SCREENSHOT_FORMAT ("Screenshot Format", "Press F12 to take a screenshot.") {
- @Override
- public String getValueString() { return screenshotFormat[screenshotFormatIndex].toUpperCase(); }
@Override
- public void click(GameContainer container) { screenshotFormatIndex = (screenshotFormatIndex + 1) % screenshotFormat.length; }
- },
- SHOW_FPS ("Show FPS Counter", "Show an FPS counter in the bottom-right hand corner.", true),
- SHOW_HIT_LIGHTING ("Show Hit Lighting", "Adds an effect behind hit explosions.", true),
- SHOW_COMBO_BURSTS ("Show Combo Bursts", "A character image is displayed at combo milestones.", true),
- SHOW_PERFECT_HIT ("Show Perfect Hits", "Whether to show perfect hit result bursts (300s, slider ticks).", true),
- SHOW_FOLLOW_POINTS ("Show Follow Points", "Whether to show follow points between hit objects.", true),
- NEW_CURSOR ("Enable New Cursor", "Use the new cursor style (may cause higher CPU usage).", true) {
+ public String write() { return Integer.toString(targetFPS[targetFPSindex]); }
+
@Override
- public void click(GameContainer container) {
- super.click(container);
- UI.resetCursor();
+ public void read(String s) {
+ int i = Integer.parseInt(s);
+ for (int j = 0; j < targetFPS.length; j++) {
+ if (i == targetFPS[j]) {
+ targetFPSindex = j;
+ break;
+ }
+ }
}
},
- DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "The song background will be used as the main menu background.", true),
- BACKGROUND_DIM ("Background Dim", "Percentage to dim the background image during gameplay.", 50, 0, 100),
- FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "Override the song background with the default playfield background.", false),
- IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "Never use skin element overrides provided by beatmaps.", false),
- FIXED_CS ("Fixed Circle Size (CS)", "Determines the size of circles and sliders.", 0, 0, 100) {
- @Override
- public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
- },
- FIXED_HP ("Fixed HP Drain Rate (HP)", "Determines the rate at which health decreases.", 0, 0, 100) {
- @Override
- public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
- },
- FIXED_AR ("Fixed Approach Rate (AR)", "Determines how long hit circles stay on the screen.", 0, 0, 100) {
- @Override
- public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
- },
- FIXED_OD ("Fixed Overall Difficulty (OD)", "Determines the time window for hit results.", 0, 0, 100) {
- @Override
- public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
- },
- LOAD_VERBOSE ("Show Detailed Loading Progress", "Display more specific loading information in the splash screen.", false),
- CHECKPOINT ("Track Checkpoint", "Press Ctrl+L while playing to load a checkpoint, and Ctrl+S to set one.", 0, 0, 3599) {
- @Override
- public String getValueString() {
- return (val == 0) ? "Disabled" : String.format("%02d:%02d",
- TimeUnit.SECONDS.toMinutes(val),
- val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
- }
- },
- DISABLE_SOUNDS ("Disable All Sound Effects", "May resolve Linux sound driver issues. Requires a restart.",
- (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
- KEY_LEFT ("Left Game Key", "Select this option to input a key.") {
- @Override
- public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
- },
- KEY_RIGHT ("Right Game Key", "Select this option to input a key.") {
- @Override
- public String getValueString() { return Keyboard.getKeyName(getGameKeyRight()); }
- },
- SHOW_UNICODE ("Prefer Non-English Metadata", "Where available, song titles will be shown in their native language.", false) {
+ SHOW_FPS ("Show FPS Counter", "FpsCounter", "Show an FPS counter in the bottom-right hand corner.", true),
+ SHOW_UNICODE ("Prefer Non-English Metadata", "ShowUnicode", "Where available, song titles will be shown in their native language.", false) {
@Override
public void click(GameContainer container) {
super.click(container);
@@ -261,15 +289,157 @@ public class Options {
}
}
},
- ENABLE_THEME_SONG ("Enable Theme Song", "Whether to play the theme song upon starting opsu!", true),
- SHOW_HIT_ERROR_BAR ("Show Hit Error Bar", "Shows precisely how accurate you were with each hit.", false),
- LOAD_HD_IMAGES ("Load HD Images", "Loads HD (@2x) images when available. Increases memory usage and loading times.", true),
- DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
- DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false);
+ SCREENSHOT_FORMAT ("Screenshot Format", "ScreenshotFormat", "Press F12 to take a screenshot.") {
+ @Override
+ public String getValueString() { return screenshotFormat[screenshotFormatIndex].toUpperCase(); }
+
+ @Override
+ public void click(GameContainer container) { screenshotFormatIndex = (screenshotFormatIndex + 1) % screenshotFormat.length; }
+
+ @Override
+ public String write() { return Integer.toString(screenshotFormatIndex); }
+
+ @Override
+ public void read(String s) {
+ int i = Integer.parseInt(s);
+ if (i >= 0 && i < screenshotFormat.length)
+ screenshotFormatIndex = i;
+ }
+ },
+ NEW_CURSOR ("Enable New Cursor", "NewCursor", "Use the new cursor style (may cause higher CPU usage).", true) {
+ @Override
+ public void click(GameContainer container) {
+ super.click(container);
+ UI.getCursor().reset();
+ }
+ },
+ DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "DynamicBackground", "The song background will be used as the main menu background.", true),
+ LOAD_VERBOSE ("Show Detailed Loading Progress", "LoadVerbose", "Display more specific loading information in the splash screen.", false),
+ MASTER_VOLUME ("Master Volume", "VolumeUniversal", "Global volume level.", 35, 0, 100) {
+ @Override
+ public void drag(GameContainer container, int d) {
+ super.drag(container, d);
+ container.setMusicVolume(getMasterVolume() * getMusicVolume());
+ }
+ },
+ MUSIC_VOLUME ("Music Volume", "VolumeMusic", "Volume of music.", 80, 0, 100) {
+ @Override
+ public void drag(GameContainer container, int d) {
+ super.drag(container, d);
+ container.setMusicVolume(getMasterVolume() * getMusicVolume());
+ }
+ },
+ EFFECT_VOLUME ("Effect Volume", "VolumeEffect", "Volume of menu and game sounds.", 70, 0, 100),
+ HITSOUND_VOLUME ("Hit Sound Volume", "VolumeHitSound", "Volume of hit sounds.", 30, 0, 100),
+ MUSIC_OFFSET ("Music Offset", "Offset", "Adjust this value if hit objects are out of sync.", -75, -500, 500) {
+ @Override
+ public String getValueString() { return String.format("%dms", val); }
+ },
+ DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.",
+ (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
+ KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
+ @Override
+ public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
+
+ @Override
+ public String write() { return Keyboard.getKeyName(getGameKeyLeft()); }
+
+ @Override
+ public void read(String s) { setGameKeyLeft(Keyboard.getKeyIndex(s)); }
+ },
+ KEY_RIGHT ("Right Game Key", "keyOsuRight", "Select this option to input a key.") {
+ @Override
+ public String getValueString() { return Keyboard.getKeyName(getGameKeyRight()); }
+
+ @Override
+ public String write() { return Keyboard.getKeyName(getGameKeyRight()); }
+
+ @Override
+ public void read(String s) { setGameKeyRight(Keyboard.getKeyIndex(s)); }
+ },
+ DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "MouseDisableWheel", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
+ DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "MouseDisableButtons", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false),
+ BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100),
+ FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false),
+ IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false),
+ SHOW_HIT_LIGHTING ("Show Hit Lighting", "HitLighting", "Adds an effect behind hit explosions.", true),
+ SHOW_COMBO_BURSTS ("Show Combo Bursts", "ComboBurst", "A character image is displayed at combo milestones.", true),
+ SHOW_PERFECT_HIT ("Show Perfect Hits", "PerfectHit", "Whether to show perfect hit result bursts (300s, slider ticks).", true),
+ SHOW_FOLLOW_POINTS ("Show Follow Points", "FollowPoints", "Whether to show follow points between hit objects.", true),
+ SHOW_HIT_ERROR_BAR ("Show Hit Error Bar", "ScoreMeter", "Shows precisely how accurate you were with each hit.", false),
+ LOAD_HD_IMAGES ("Load HD Images", "LoadHDImages", String.format("Loads HD (%s) images when available. Increases memory usage and loading times.", GameImage.HD_SUFFIX), true),
+ FIXED_CS ("Fixed Circle Size (CS)", "FixedCS", "Determines the size of circles and sliders.", 0, 0, 100) {
+ @Override
+ public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
+
+ @Override
+ public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
+
+ @Override
+ public void read(String s) {
+ int i = (int) (Float.parseFloat(s) * 10f);
+ if (i >= 0 && i <= 100)
+ val = i;
+ }
+ },
+ FIXED_HP ("Fixed HP Drain Rate (HP)", "FixedHP", "Determines the rate at which health decreases.", 0, 0, 100) {
+ @Override
+ public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
+
+ @Override
+ public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
+
+ @Override
+ public void read(String s) {
+ int i = (int) (Float.parseFloat(s) * 10f);
+ if (i >= 0 && i <= 100)
+ val = i;
+ }
+ },
+ FIXED_AR ("Fixed Approach Rate (AR)", "FixedAR", "Determines how long hit circles stay on the screen.", 0, 0, 100) {
+ @Override
+ public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
+
+ @Override
+ public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
+
+ @Override
+ public void read(String s) {
+ int i = (int) (Float.parseFloat(s) * 10f);
+ if (i >= 0 && i <= 100)
+ val = i;
+ }
+ },
+ FIXED_OD ("Fixed Overall Difficulty (OD)", "FixedOD", "Determines the time window for hit results.", 0, 0, 100) {
+ @Override
+ public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
+
+ @Override
+ public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
+
+ @Override
+ public void read(String s) {
+ int i = (int) (Float.parseFloat(s) * 10f);
+ if (i >= 0 && i <= 100)
+ val = i;
+ }
+ },
+ CHECKPOINT ("Track Checkpoint", "Checkpoint", "Press Ctrl+L while playing to load a checkpoint, and Ctrl+S to set one.", 0, 0, 3599) {
+ @Override
+ public String getValueString() {
+ return (val == 0) ? "Disabled" : String.format("%02d:%02d",
+ TimeUnit.SECONDS.toMinutes(val),
+ val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
+ }
+ },
+ ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true);
/** Option name. */
private String name;
+ /** Option name, as displayed in the configuration file. */
+ private String displayName;
+
/** Option description. */
private String description;
@@ -282,43 +452,58 @@ public class Options {
/** The upper and lower bounds on the integer value (if applicable). */
private int max, min;
+ /** Option types. */
+ private enum OptionType { BOOLEAN, NUMERIC, OTHER };
+
/** Whether or not this is a numeric option. */
- private boolean isNumeric;
+ private OptionType type = OptionType.OTHER;
/**
- * Constructor.
+ * Constructor for internal options (not displayed in-game).
+ * @param displayName the option name, as displayed in the configuration file
+ */
+ GameOption(String displayName) {
+ this.displayName = displayName;
+ }
+
+ /**
+ * Constructor for other option types.
* @param name the option name
+ * @param displayName the option name, as displayed in the configuration file
* @param description the option description
*/
- GameOption(String name, String description) {
+ GameOption(String name, String displayName, String description) {
this.name = name;
+ this.displayName = displayName;
this.description = description;
}
/**
- * Constructor.
+ * Constructor for boolean options.
* @param name the option name
+ * @param displayName the option name, as displayed in the configuration file
* @param description the option description
* @param value the default boolean value
*/
- GameOption(String name, String description, boolean value) {
- this(name, description);
+ GameOption(String name, String displayName, String description, boolean value) {
+ this(name, displayName, description);
this.bool = value;
- this.isNumeric = false;
+ this.type = OptionType.BOOLEAN;
}
/**
- * Constructor.
+ * Constructor for numeric options.
* @param name the option name
+ * @param displayName the option name, as displayed in the configuration file
* @param description the option description
* @param value the default integer value
*/
- GameOption(String name, String description, int value, int min, int max) {
- this(name, description);
+ GameOption(String name, String displayName, String description, int value, int min, int max) {
+ this(name, displayName, description);
this.val = value;
this.min = min;
this.max = max;
- this.isNumeric = true;
+ this.type = OptionType.NUMERIC;
}
/**
@@ -327,6 +512,12 @@ public class Options {
*/
public String getName() { return name; }
+ /**
+ * Returns the option name, as displayed in the configuration file.
+ * @return the display name string
+ */
+ public String getDisplayName() { return displayName; }
+
/**
* Returns the option description.
* @return the description string
@@ -360,15 +551,18 @@ public class Options {
/**
* Returns the value of the option as a string (via override).
*
- * By default, this returns "{@code val}%" if this is an numeric option,
- * or "Yes" or "No" based on the {@code bool} field otherwise.
+ * By default, this returns "{@code val}%" for numeric options,
+ * "Yes" or "No" based on the {@code bool} field for boolean options,
+ * and an empty string otherwise.
* @return the value string
*/
public String getValueString() {
- if (isNumeric)
+ if (type == OptionType.NUMERIC)
return String.format("%d%%", val);
- else
+ else if (type == OptionType.BOOLEAN)
return (bool) ? "Yes" : "No";
+ else
+ return "";
}
/**
@@ -382,17 +576,54 @@ public class Options {
/**
* Processes a mouse drag action (via override).
*
- * By default, if this is a numeric option, the {@code val} field will
- * be shifted by {@code d} within the given bounds.
+ * By default, only if this is a numeric option, the {@code val} field
+ * will be shifted by {@code d} within the given bounds.
* @param container the game container
* @param d the dragged distance (modified by multiplier)
*/
public void drag(GameContainer container, int d) {
- if (isNumeric)
+ if (type == OptionType.NUMERIC)
val = Utils.getBoundedValue(val, d, min, max);
}
+
+ /**
+ * Returns the string to write to the configuration file (via override).
+ *
+ * By default, this returns "{@code val}" for numeric options,
+ * "true" or "false" based on the {@code bool} field for boolean options,
+ * and {@link #getValueString()} otherwise.
+ * @return the string to write
+ */
+ public String write() {
+ if (type == OptionType.NUMERIC)
+ return Integer.toString(val);
+ else if (type == OptionType.BOOLEAN)
+ return Boolean.toString(bool);
+ else
+ return getValueString();
+ }
+
+ /**
+ * Reads the value of the option from the configuration file (via override).
+ *
+ * By default, this sets {@code val} for numeric options only if the
+ * value is between the min and max bounds, sets {@code bool} for
+ * boolean options, and does nothing otherwise.
+ * @param s the value string read from the configuration file
+ */
+ public void read(String s) {
+ if (type == OptionType.NUMERIC) {
+ int i = Integer.parseInt(s);
+ if (i >= min && i <= max)
+ val = i;
+ } else if (type == OptionType.BOOLEAN)
+ bool = Boolean.parseBoolean(s);
+ }
};
+ /** Map of option display names to GameOptions. */
+ private static HashMap optionMap;
+
/** Screen resolutions. */
private enum Resolution {
RES_800_600 (800, 600),
@@ -451,6 +682,18 @@ public class Options {
/** Current screen resolution. */
private static Resolution resolution = Resolution.RES_1024_768;
+ /** The available skin directories. */
+ private static String[] skinDirs;
+
+ /** The index in the skinDirs array. */
+ private static int skinDirIndex = 0;
+
+ /** The name of the skin. */
+ private static String skinName = "Default";
+
+ /** The current skin. */
+ private static Skin skin;
+
/** Frame limiters. */
private static final int[] targetFPS = { 60, 120, 240, 30, 20, 15, 12 };
@@ -872,38 +1115,102 @@ public class Options {
* If invalid, this will create a "Skins" folder in the root directory.
* @return the skin directory
*/
- public static File getSkinDir() {
- if (skinDir != null && skinDir.isDirectory())
- return skinDir;
+ public static File getSkinRootDir() {
+ if (skinRootDir != null && skinRootDir.isDirectory())
+ return skinRootDir;
- skinDir = new File(DATA_DIR, "Skins/");
- skinDir.mkdir();
- return skinDir;
+ // search for directory
+ for (int i = 0; i < SKIN_ROOT_DIRS.length; i++) {
+ skinRootDir = new File(SKIN_ROOT_DIRS[i]);
+ if (skinRootDir.isDirectory())
+ return skinRootDir;
+ }
+ skinRootDir.mkdir(); // none found, create new directory
+ return skinRootDir;
}
/**
- * Returns a dummy OsuFile containing the theme song.
- * @return the theme song OsuFile
+ * Loads the skin given by the current skin directory.
+ * If the directory is invalid, the default skin will be loaded.
*/
- public static OsuFile getOsuTheme() {
+ public static void loadSkin() {
+ File skinDir = getSkinDir();
+ if (skinDir == null) // invalid skin name
+ skinName = Skin.DEFAULT_SKIN_NAME;
+
+ // create available skins list
+ File[] dirs = SkinLoader.getSkinDirectories(getSkinRootDir());
+ skinDirs = new String[dirs.length + 1];
+ skinDirs[0] = Skin.DEFAULT_SKIN_NAME;
+ for (int i = 0; i < dirs.length; i++)
+ skinDirs[i + 1] = dirs[i].getName();
+
+ // set skin and modify resource locations
+ ResourceLoader.removeAllResourceLocations();
+ if (skinDir == null)
+ skin = new Skin(null);
+ else {
+ // set skin index
+ for (int i = 1; i < skinDirs.length; i++) {
+ if (skinDirs[i].equals(skinName)) {
+ skinDirIndex = i;
+ break;
+ }
+ }
+
+ // load the skin
+ skin = SkinLoader.loadSkin(skinDir);
+ ResourceLoader.addResourceLocation(new FileSystemLocation(skinDir));
+ }
+ ResourceLoader.addResourceLocation(new ClasspathLocation());
+ ResourceLoader.addResourceLocation(new FileSystemLocation(new File(".")));
+ ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
+ }
+
+ /**
+ * Returns the current skin.
+ * @return the skin, or null if no skin is loaded (see {@link #loadSkin()})
+ */
+ public static Skin getSkin() { return skin; }
+
+ /**
+ * Returns the current skin directory.
+ *
+ * NOTE: This directory will differ from that of the currently loaded skin
+ * if {@link #loadSkin()} has not been called after a directory change.
+ * Use {@link Skin#getDirectory()} to get the directory of the currently
+ * loaded skin.
+ * @return the skin directory, or null for the default skin
+ */
+ public static File getSkinDir() {
+ File root = getSkinRootDir();
+ File dir = new File(root, skinName);
+ return (dir.isDirectory()) ? dir : null;
+ }
+
+ /**
+ * Returns a dummy Beatmap containing the theme song.
+ * @return the theme song beatmap
+ */
+ public static Beatmap getThemeBeatmap() {
String[] tokens = themeString.split(",");
if (tokens.length != 4) {
ErrorHandler.error("Theme song string is malformed.", null, false);
return null;
}
- OsuFile osu = new OsuFile(null);
- osu.audioFilename = new File(tokens[0]);
- osu.title = tokens[1];
- osu.artist = tokens[2];
+ Beatmap beatmap = new Beatmap(null);
+ beatmap.audioFilename = new File(tokens[0]);
+ beatmap.title = tokens[1];
+ beatmap.artist = tokens[2];
try {
- osu.endTime = Integer.parseInt(tokens[3]);
+ beatmap.endTime = Integer.parseInt(tokens[3]);
} catch (NumberFormatException e) {
ErrorHandler.error("Theme song length is not a valid integer", e, false);
return null;
}
- return osu;
+ return beatmap;
}
/**
@@ -916,10 +1223,16 @@ public class Options {
return;
}
+ // create option map
+ if (optionMap == null) {
+ optionMap = new HashMap();
+ for (GameOption option : GameOption.values())
+ optionMap.put(option.getDisplayName(), option);
+ }
+
+ // read file
try (BufferedReader in = new BufferedReader(new FileReader(OPTIONS_FILE))) {
String line;
- String name, value;
- int i;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.length() < 2 || line.charAt(0) == '#')
@@ -927,160 +1240,17 @@ public class Options {
int index = line.indexOf('=');
if (index == -1)
continue;
- name = line.substring(0, index).trim();
- value = line.substring(index + 1).trim();
- try {
- switch (name) {
- case "BeatmapDirectory":
- beatmapDir = new File(value);
- break;
- case "OSZDirectory":
- oszDir = new File(value);
- break;
- case "ScreenshotDirectory":
- screenshotDir = new File(value);
- break;
- case "ReplayDirectory":
- replayDir = new File(value);
- break;
- case "Skin":
- skinDir = new File(value);
- break;
- case "ThemeSong":
- themeString = value;
- break;
- case "Port":
- i = Integer.parseInt(value);
- if (i > 0 && i <= 65535)
- port = i;
- break;
- case "ScreenResolution":
- try {
- Resolution res = Resolution.valueOf(String.format("RES_%s", value.replace('x', '_')));
- resolution = res;
- } catch (IllegalArgumentException e) {}
- break;
-// case "Fullscreen":
-// GameOption.FULLSCREEN.setValue(Boolean.parseBoolean(value));
-// break;
- case "FrameSync":
- i = Integer.parseInt(value);
- for (int j = 0; j < targetFPS.length; j++) {
- if (i == targetFPS[j])
- targetFPSindex = j;
- }
- break;
- case "ScreenshotFormat":
- i = Integer.parseInt(value);
- if (i >= 0 && i < screenshotFormat.length)
- screenshotFormatIndex = i;
- break;
- case "FpsCounter":
- GameOption.SHOW_FPS.setValue(Boolean.parseBoolean(value));
- break;
- case "ShowUnicode":
- GameOption.SHOW_UNICODE.setValue(Boolean.parseBoolean(value));
- break;
- case "NewCursor":
- GameOption.NEW_CURSOR.setValue(Boolean.parseBoolean(value));
- break;
- case "DynamicBackground":
- GameOption.DYNAMIC_BACKGROUND.setValue(Boolean.parseBoolean(value));
- break;
- case "LoadVerbose":
- GameOption.LOAD_VERBOSE.setValue(Boolean.parseBoolean(value));
- break;
- case "VolumeUniversal":
- i = Integer.parseInt(value);
- if (i >= 0 && i <= 100)
- GameOption.MASTER_VOLUME.setValue(i);
- break;
- case "VolumeMusic":
- i = Integer.parseInt(value);
- if (i >= 0 && i <= 100)
- GameOption.MUSIC_VOLUME.setValue(i);
- break;
- case "VolumeEffect":
- i = Integer.parseInt(value);
- if (i >= 0 && i <= 100)
- GameOption.EFFECT_VOLUME.setValue(i);
- break;
- case "VolumeHitSound":
- i = Integer.parseInt(value);
- if (i >= 0 && i <= 100)
- GameOption.HITSOUND_VOLUME.setValue(i);
- break;
- case "Offset":
- i = Integer.parseInt(value);
- if (i >= -500 && i <= 500)
- GameOption.MUSIC_OFFSET.setValue(i);
- break;
- case "DisableSound":
- GameOption.DISABLE_SOUNDS.setValue(Boolean.parseBoolean(value));
- break;
- case "keyOsuLeft":
- setGameKeyLeft(Keyboard.getKeyIndex(value));
- break;
- case "keyOsuRight":
- setGameKeyRight(Keyboard.getKeyIndex(value));
- break;
- case "MouseDisableWheel":
- GameOption.DISABLE_MOUSE_WHEEL.setValue(Boolean.parseBoolean(value));
- break;
- case "MouseDisableButtons":
- GameOption.DISABLE_MOUSE_BUTTONS.setValue(Boolean.parseBoolean(value));
- break;
- case "DimLevel":
- i = Integer.parseInt(value);
- if (i >= 0 && i <= 100)
- GameOption.BACKGROUND_DIM.setValue(i);
- break;
- case "ForceDefaultPlayfield":
- GameOption.FORCE_DEFAULT_PLAYFIELD.setValue(Boolean.parseBoolean(value));
- break;
- case "IgnoreBeatmapSkins":
- GameOption.IGNORE_BEATMAP_SKINS.setValue(Boolean.parseBoolean(value));
- break;
- case "HitLighting":
- GameOption.SHOW_HIT_LIGHTING.setValue(Boolean.parseBoolean(value));
- break;
- case "ComboBurst":
- GameOption.SHOW_COMBO_BURSTS.setValue(Boolean.parseBoolean(value));
- break;
- case "PerfectHit":
- GameOption.SHOW_PERFECT_HIT.setValue(Boolean.parseBoolean(value));
- break;
- case "FollowPoints":
- GameOption.SHOW_FOLLOW_POINTS.setValue(Boolean.parseBoolean(value));
- break;
- case "ScoreMeter":
- GameOption.SHOW_HIT_ERROR_BAR.setValue(Boolean.parseBoolean(value));
- break;
- case "LoadHDImages":
- GameOption.LOAD_HD_IMAGES.setValue(Boolean.parseBoolean(value));
- break;
- case "FixedCS":
- GameOption.FIXED_CS.setValue((int) (Float.parseFloat(value) * 10f));
- break;
- case "FixedHP":
- GameOption.FIXED_HP.setValue((int) (Float.parseFloat(value) * 10f));
- break;
- case "FixedAR":
- GameOption.FIXED_AR.setValue((int) (Float.parseFloat(value) * 10f));
- break;
- case "FixedOD":
- GameOption.FIXED_OD.setValue((int) (Float.parseFloat(value) * 10f));
- break;
- case "Checkpoint":
- setCheckpoint(Integer.parseInt(value));
- break;
- case "MenuMusic":
- GameOption.ENABLE_THEME_SONG.setValue(Boolean.parseBoolean(value));
- break;
+
+ // read option
+ String name = line.substring(0, index).trim();
+ GameOption option = optionMap.get(name);
+ if (option != null) {
+ try {
+ String value = line.substring(index + 1).trim();
+ option.read(value);
+ } catch (NumberFormatException e) {
+ Log.warn(String.format("Format error in options file for line: '%s'.", line), e);
}
- } catch (NumberFormatException e) {
- Log.warn(String.format("Format error in options file for line: '%s'.", line), e);
- continue;
}
}
} catch (IOException e) {
@@ -1105,88 +1275,12 @@ public class Options {
writer.newLine();
// options
- writer.write(String.format("BeatmapDirectory = %s", getBeatmapDir().getAbsolutePath()));
- writer.newLine();
- writer.write(String.format("OSZDirectory = %s", getOSZDir().getAbsolutePath()));
- writer.newLine();
- writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath()));
- writer.newLine();
- writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath()));
- writer.newLine();
- writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath()));
- writer.newLine();
- writer.write(String.format("ThemeSong = %s", themeString));
- writer.newLine();
- writer.write(String.format("Port = %d", port));
- writer.newLine();
- writer.write(String.format("ScreenResolution = %s", resolution.toString()));
- writer.newLine();
-// writer.write(String.format("Fullscreen = %b", isFullscreen()));
-// writer.newLine();
- writer.write(String.format("FrameSync = %d", targetFPS[targetFPSindex]));
- writer.newLine();
- writer.write(String.format("FpsCounter = %b", isFPSCounterEnabled()));
- writer.newLine();
- writer.write(String.format("ShowUnicode = %b", useUnicodeMetadata()));
- writer.newLine();
- writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex));
- writer.newLine();
- writer.write(String.format("NewCursor = %b", isNewCursorEnabled()));
- writer.newLine();
- writer.write(String.format("DynamicBackground = %b", isDynamicBackgroundEnabled()));
- writer.newLine();
- writer.write(String.format("LoadVerbose = %b", isLoadVerbose()));
- writer.newLine();
- writer.write(String.format("VolumeUniversal = %d", GameOption.MASTER_VOLUME.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("VolumeMusic = %d", GameOption.MUSIC_VOLUME.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("VolumeEffect = %d", GameOption.EFFECT_VOLUME.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("VolumeHitSound = %d", GameOption.HITSOUND_VOLUME.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("Offset = %d", getMusicOffset()));
- writer.newLine();
- writer.write(String.format("DisableSound = %b", isSoundDisabled()));
- writer.newLine();
- writer.write(String.format("keyOsuLeft = %s", Keyboard.getKeyName(getGameKeyLeft())));
- writer.newLine();
- writer.write(String.format("keyOsuRight = %s", Keyboard.getKeyName(getGameKeyRight())));
- writer.newLine();
- writer.write(String.format("MouseDisableWheel = %b", isMouseWheelDisabled()));
- writer.newLine();
- writer.write(String.format("MouseDisableButtons = %b", isMouseDisabled()));
- writer.newLine();
- writer.write(String.format("DimLevel = %d", GameOption.BACKGROUND_DIM.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("ForceDefaultPlayfield = %b", isDefaultPlayfieldForced()));
- writer.newLine();
- writer.write(String.format("IgnoreBeatmapSkins = %b", isBeatmapSkinIgnored()));
- writer.newLine();
- writer.write(String.format("HitLighting = %b", isHitLightingEnabled()));
- writer.newLine();
- writer.write(String.format("ComboBurst = %b", isComboBurstEnabled()));
- writer.newLine();
- writer.write(String.format("PerfectHit = %b", isPerfectHitBurstEnabled()));
- writer.newLine();
- writer.write(String.format("FollowPoints = %b", isFollowPointEnabled()));
- writer.newLine();
- writer.write(String.format("ScoreMeter = %b", isHitErrorBarEnabled()));
- writer.newLine();
- writer.write(String.format("LoadHDImages = %b", loadHDImages()));
- writer.newLine();
- writer.write(String.format(Locale.US, "FixedCS = %.1f", getFixedCS()));
- writer.newLine();
- writer.write(String.format(Locale.US, "FixedHP = %.1f", getFixedHP()));
- writer.newLine();
- writer.write(String.format(Locale.US, "FixedAR = %.1f", getFixedAR()));
- writer.newLine();
- writer.write(String.format(Locale.US, "FixedOD = %.1f", getFixedOD()));
- writer.newLine();
- writer.write(String.format("Checkpoint = %d", GameOption.CHECKPOINT.getIntegerValue()));
- writer.newLine();
- writer.write(String.format("MenuMusic = %b", isThemeSongEnabled()));
- writer.newLine();
+ for (GameOption option : GameOption.values()) {
+ writer.write(option.getDisplayName());
+ writer.write(" = ");
+ writer.write(option.write());
+ writer.newLine();
+ }
writer.close();
} catch (IOException e) {
ErrorHandler.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false);
diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java
index 9e01abc3..dd7c9436 100644
--- a/src/itdelatrisu/opsu/OsuFile.java
+++ b/src/itdelatrisu/opsu/OsuFile.java
@@ -1,3 +1,4 @@
+//TODO rename
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/OsuGroupList.java b/src/itdelatrisu/opsu/OsuGroupList.java
index b7bbbe50..54aa09ba 100644
--- a/src/itdelatrisu/opsu/OsuGroupList.java
+++ b/src/itdelatrisu/opsu/OsuGroupList.java
@@ -1,3 +1,5 @@
+//TODO rename
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/OsuGroupNode.java b/src/itdelatrisu/opsu/OsuGroupNode.java
deleted file mode 100644
index 8c8e15d2..00000000
--- a/src/itdelatrisu/opsu/OsuGroupNode.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * 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 .
- */
-
-package itdelatrisu.opsu;
-
-import itdelatrisu.opsu.GameData.Grade;
-
-import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
-
-import org.newdawn.slick.Color;
-import org.newdawn.slick.Image;
-
-/**
- * Node in an OsuGroupList representing a group of OsuFile objects.
- */
-public class OsuGroupNode {
- /** List of associated OsuFile objects. */
- public ArrayList osuFiles;
-
- /** Index of this OsuGroupNode. */
- public int index = 0;
-
- /** Index of selected osuFile (-1 if not focused). */
- public int osuFileIndex = -1;
-
- /** Links to other OsuGroupNode objects. */
- public OsuGroupNode prev, next;
-
- /**
- * Constructor.
- * @param osuFiles the OsuFile objects in this group
- */
- public OsuGroupNode(ArrayList osuFiles) {
- this.osuFiles = osuFiles;
- }
-
- /**
- * Draws the button.
- * @param x the x coordinate
- * @param y the y coordinate
- * @param grade the highest grade, if any
- * @param focus true if this is the focused node
- */
- public void draw(float x, float y, Grade grade, boolean focus) {
- Image bg = GameImage.MENU_BUTTON_BG.getImage();
- boolean expanded = (osuFileIndex > -1);
- OsuFile osu;
- bg.setAlpha(0.9f);
- Color bgColor;
- Color textColor = Color.lightGray;
-
- // get drawing parameters
- if (expanded) {
- x -= bg.getWidth() / 10f;
- if (focus) {
- bgColor = Color.white;
- textColor = Color.white;
- } else
- bgColor = Utils.COLOR_BLUE_BUTTON;
- osu = osuFiles.get(osuFileIndex);
- } else {
- bgColor = Utils.COLOR_ORANGE_BUTTON;
- osu = osuFiles.get(0);
- }
- bg.draw(x, y, bgColor);
-
- float cx = x + (bg.getWidth() * 0.05f);
- float cy = y + (bg.getHeight() * 0.2f) - 3;
-
- // draw grade
- if (grade != Grade.NULL) {
- Image gradeImg = grade.getMenuImage();
- gradeImg.drawCentered(cx - bg.getWidth() * 0.01f + gradeImg.getWidth() / 2f, y + bg.getHeight() / 2.2f);
- cx += gradeImg.getWidth();
- }
-
- // draw text
- if (Options.useUnicodeMetadata()) { // load glyphs
- Utils.loadGlyphs(Utils.FONT_MEDIUM, osu.titleUnicode, null);
- Utils.loadGlyphs(Utils.FONT_DEFAULT, null, osu.artistUnicode);
- }
- Utils.FONT_MEDIUM.drawString(cx, cy, osu.getTitle(), textColor);
- Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 4,
- String.format("%s // %s", osu.getArtist(), osu.creator), textColor);
- if (expanded || osuFiles.size() == 1)
- Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8,
- osu.version, textColor);
- }
-
- /**
- * Returns an array of strings containing song information.
- *
- * - 0: {Artist} - {Title} [{Version}]
- *
- 1: Mapped by {Creator}
- *
- 2: Length: {} BPM: {} Objects: {}
- *
- 3: Circles: {} Sliders: {} Spinners: {}
- *
- 4: CS:{} HP:{} AR:{} OD:{}
- *
- */
- public String[] getInfo() {
- if (osuFileIndex < 0)
- return null;
-
- OsuFile osu = osuFiles.get(osuFileIndex);
- String[] info = new String[5];
- info[0] = osu.toString();
- info[1] = String.format("Mapped by %s",
- osu.creator);
- info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d",
- TimeUnit.MILLISECONDS.toMinutes(osu.endTime),
- TimeUnit.MILLISECONDS.toSeconds(osu.endTime) -
- TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(osu.endTime)),
- (osu.bpmMax <= 0) ? "--" :
- ((osu.bpmMin == osu.bpmMax) ? osu.bpmMin : String.format("%d-%d", osu.bpmMin, osu.bpmMax)),
- (osu.hitObjectCircle + osu.hitObjectSlider + osu.hitObjectSpinner));
- info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
- osu.hitObjectCircle, osu.hitObjectSlider, osu.hitObjectSpinner);
- info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f",
- osu.circleSize, osu.HPDrainRate, osu.approachRate, osu.overallDifficulty);
- return info;
- }
-
- /**
- * Returns a formatted string for the OsuFile at osuFileIndex:
- * "Artist - Title [Version]" (version omitted if osuFileIndex is invalid)
- * @see java.lang.Object#toString()
- */
- @Override
- public String toString() {
- if (osuFileIndex == -1)
- return String.format("%s - %s", osuFiles.get(0).getArtist(), osuFiles.get(0).getTitle());
- else
- return osuFiles.get(osuFileIndex).toString();
- }
-
- /**
- * Checks whether the node matches a given search query.
- * @param query the search term
- * @return true if title, artist, creator, source, version, or tag matches query
- */
- public boolean matches(String query) {
- OsuFile osu = osuFiles.get(0);
-
- // search: title, artist, creator, source, version, tags (first OsuFile)
- if (osu.title.toLowerCase().contains(query) ||
- osu.titleUnicode.toLowerCase().contains(query) ||
- osu.artist.toLowerCase().contains(query) ||
- osu.artistUnicode.toLowerCase().contains(query) ||
- osu.creator.toLowerCase().contains(query) ||
- osu.source.toLowerCase().contains(query) ||
- osu.version.toLowerCase().contains(query) ||
- osu.tags.contains(query))
- return true;
-
- // search: version, tags (remaining OsuFiles)
- for (int i = 1; i < osuFiles.size(); i++) {
- osu = osuFiles.get(i);
- if (osu.version.toLowerCase().contains(query) ||
- osu.tags.contains(query))
- return true;
- }
-
- return false;
- }
-
- /**
- * Checks whether the node matches a given condition.
- * @param type the condition type (ar, cs, od, hp, bpm, length)
- * @param operator the operator (=/==, >, >=, <, <=)
- * @param value the value
- * @return true if the condition is met
- */
- public boolean matches(String type, String operator, float value) {
- for (OsuFile osu : osuFiles) {
- // get value
- float osuValue;
- switch (type) {
- case "ar": osuValue = osu.approachRate; break;
- case "cs": osuValue = osu.circleSize; break;
- case "od": osuValue = osu.overallDifficulty; break;
- case "hp": osuValue = osu.HPDrainRate; break;
- case "bpm": osuValue = osu.bpmMax; break;
- case "length": osuValue = osu.endTime / 1000; break;
- default: return false;
- }
-
- // get operator
- boolean met;
- switch (operator) {
- case "=":
- case "==": met = (osuValue == value); break;
- case ">": met = (osuValue > value); break;
- case ">=": met = (osuValue >= value); break;
- case "<": met = (osuValue < value); break;
- case "<=": met = (osuValue <= value); break;
- default: return false;
- }
-
- if (met)
- return true;
- }
-
- return false;
- }
-}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/OsuHitObject.java b/src/itdelatrisu/opsu/OsuHitObject.java
index ac641a7a..4b62bdce 100644
--- a/src/itdelatrisu/opsu/OsuHitObject.java
+++ b/src/itdelatrisu/opsu/OsuHitObject.java
@@ -1,3 +1,5 @@
+//TODO rename
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/OsuParser.java b/src/itdelatrisu/opsu/OsuParser.java
index a3b2c0ab..715e90d4 100644
--- a/src/itdelatrisu/opsu/OsuParser.java
+++ b/src/itdelatrisu/opsu/OsuParser.java
@@ -1,3 +1,5 @@
+//TODO rename
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java
index 7abb055d..b6307031 100644
--- a/src/itdelatrisu/opsu/ScoreData.java
+++ b/src/itdelatrisu/opsu/ScoreData.java
@@ -20,6 +20,7 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.states.SongMenu;
+import itdelatrisu.opsu.ui.UI;
import java.sql.ResultSet;
import java.sql.SQLException;
diff --git a/src/itdelatrisu/opsu/UI.java b/src/itdelatrisu/opsu/UI.java
index bbffec46..1f7aa07d 100644
--- a/src/itdelatrisu/opsu/UI.java
+++ b/src/itdelatrisu/opsu/UI.java
@@ -1,3 +1,5 @@
+//TODO rename?
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java
index 61baf19a..feb24d44 100644
--- a/src/itdelatrisu/opsu/Utils.java
+++ b/src/itdelatrisu/opsu/Utils.java
@@ -20,8 +20,11 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadNode;
+import itdelatrisu.opsu.replay.PlaybackSpeed;
+import itdelatrisu.opsu.ui.UI;
import java.awt.Font;
import java.awt.image.BufferedImage;
@@ -34,6 +37,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
@@ -49,6 +53,9 @@ import java.util.Scanner;
import javax.imageio.ImageIO;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
@@ -78,10 +85,6 @@ public class Utils {
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
COLOR_BLUE_BUTTON = new Color(40, 129, 237),
COLOR_ORANGE_BUTTON = new Color(200, 90, 3),
- COLOR_GREEN_OBJECT = new Color(26, 207, 26),
- COLOR_BLUE_OBJECT = new Color(46, 136, 248),
- COLOR_RED_OBJECT = new Color(243, 48, 77),
- COLOR_ORANGE_OBJECT = new Color(255, 200, 32),
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
COLOR_RED_HOVER = new Color(255, 112, 112),
@@ -90,13 +93,9 @@ public class Utils {
COLOR_LIGHT_GREEN = new Color(128,255,128),
COLOR_LIGHT_BLUE = new Color(128,128,255),
COLOR_GREEN_SEARCH = new Color(173, 255, 47),
- COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f);
-
- /** The default map colors, used when a map does not provide custom colors. */
- public static final Color[] DEFAULT_COMBO = {
- COLOR_ORANGE_OBJECT, COLOR_GREEN_OBJECT,
- COLOR_BLUE_OBJECT, COLOR_RED_OBJECT,
- };
+ COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
+ COLOR_RED_HIGHLIGHT = new Color(246, 154, 161),
+ COLOR_BLUE_HIGHLIGHT = new Color(173, 216, 230);
/** Game fonts. */
public static UnicodeFont
@@ -161,16 +160,19 @@ public class Utils {
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
- loadFont(FONT_DEFAULT, 2, colorEffect);
- loadFont(FONT_BOLD, 2, colorEffect);
- loadFont(FONT_XLARGE, 4, colorEffect);
- loadFont(FONT_LARGE, 4, colorEffect);
- loadFont(FONT_MEDIUM, 3, colorEffect);
- loadFont(FONT_SMALL, 1, colorEffect);
+ loadFont(FONT_DEFAULT, colorEffect);
+ loadFont(FONT_BOLD, colorEffect);
+ loadFont(FONT_XLARGE, colorEffect);
+ loadFont(FONT_LARGE, colorEffect);
+ loadFont(FONT_MEDIUM, colorEffect);
+ loadFont(FONT_SMALL, colorEffect);
} catch (Exception e) {
ErrorHandler.error("Failed to load fonts.", e, true);
}
+ // load skin
+ Options.loadSkin();
+
// initialize game images
for (GameImage img : GameImage.values()) {
if (img.isPreload())
@@ -180,8 +182,11 @@ public class Utils {
// initialize game mods
GameMod.init(width, height);
+ // initialize playback buttons
+ PlaybackSpeed.init(width, height);
+
// initialize hit objects
- OsuHitObject.init(width, height);
+ HitObject.init(width, height);
// initialize download nodes
DownloadNode.init(width, height);
@@ -331,15 +336,11 @@ public class Utils {
/**
* Loads a Unicode font.
* @param font the font to load
- * @param padding the top and bottom padding
* @param effect the font effect
* @throws SlickException
*/
@SuppressWarnings("unchecked")
- private static void loadFont(UnicodeFont font, int padding,
- Effect effect) throws SlickException {
- font.setPaddingTop(padding);
- font.setPaddingBottom(padding);
+ private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
@@ -516,6 +517,7 @@ public class Utils {
* Returns a the contents of a URL as a string.
* @param url the remote URL
* @return the contents as a string, or null if any error occurred
+ * @author Roland Illig (http://stackoverflow.com/a/4308662)
*/
public static String readDataFromUrl(URL url) throws IOException {
// open connection
@@ -547,6 +549,42 @@ public class Utils {
}
}
+ /**
+ * Returns a JSON object from a URL.
+ * @param url the remote URL
+ * @return the JSON object, or null if an error occurred
+ */
+ public static JSONObject readJsonObjectFromUrl(URL url) throws IOException {
+ String s = Utils.readDataFromUrl(url);
+ JSONObject json = null;
+ if (s != null) {
+ try {
+ json = new JSONObject(s);
+ } catch (JSONException e) {
+ ErrorHandler.error("Failed to create JSON object.", e, true);
+ }
+ }
+ return json;
+ }
+
+ /**
+ * Returns a JSON array from a URL.
+ * @param url the remote URL
+ * @return the JSON array, or null if an error occurred
+ */
+ public static JSONArray readJsonArrayFromUrl(URL url) throws IOException {
+ String s = Utils.readDataFromUrl(url);
+ JSONArray json = null;
+ if (s != null) {
+ try {
+ json = new JSONArray(s);
+ } catch (JSONException e) {
+ ErrorHandler.error("Failed to create JSON array.", e, true);
+ }
+ }
+ return json;
+ }
+
/**
* Converts an input stream to a string.
* @param is the input stream
@@ -601,4 +639,60 @@ public class Utils {
else
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60);
}
+
+ /**
+ * Cubic ease out function.
+ * @param t the current time
+ * @param a the starting position
+ * @param b the finishing position
+ * @param d the duration
+ * @return the eased float
+ */
+ public static float easeOut(float t, float a, float b, float d) {
+ return b * ((t = t / d - 1f) * t * t + 1f) + a;
+ }
+
+ /**
+ * Fake bounce ease function.
+ * @param t the current time
+ * @param a the starting position
+ * @param b the finishing position
+ * @param d the duration
+ * @return the eased float
+ */
+ public static float easeBounce(float t, float a, float b, float d) {
+ if (t < d / 2)
+ return easeOut(t, a, b, d);
+ return easeOut(d - t, a, b, d);
+ }
+
+ /**
+ * Returns whether or not the application is running within a JAR.
+ * @return true if JAR, false if file
+ */
+ public static boolean isJarRunning() {
+ return Opsu.class.getResource(String.format("%s.class", Opsu.class.getSimpleName())).toString().startsWith("jar:");
+ }
+
+ /**
+ * Returns the directory where the application is being run.
+ */
+ public static File getRunningDirectory() {
+ try {
+ return new File(Opsu.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
+ } catch (URISyntaxException e) {
+ Log.error("Could not get the running directory.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Parses the integer string argument as a boolean:
+ * {@code 1} is {@code true}, and all other values are {@code false}.
+ * @param s the {@code String} containing the boolean representation to be parsed
+ * @return the boolean represented by the string argument
+ */
+ public static boolean parseBoolean(String s) {
+ return (Integer.parseInt(s) == 1);
+ }
}
diff --git a/src/itdelatrisu/opsu/audio/HitSound.java b/src/itdelatrisu/opsu/audio/HitSound.java
index ab2457d5..590f3ddb 100644
--- a/src/itdelatrisu/opsu/audio/HitSound.java
+++ b/src/itdelatrisu/opsu/audio/HitSound.java
@@ -105,6 +105,12 @@ public enum HitSound implements SoundController.SoundComponent {
return (currentSampleSet != null) ? clips.get(currentSampleSet) : null;
}
+ /**
+ * Returns the Clip associated with the given sample set.
+ * @param s the sample set
+ */
+ public MultiClip getClip(SampleSet s) { return clips.get(s); }
+
/**
* Sets the hit sound Clip for the sample type.
* @param s the sample set
diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java
index 7c93c15d..695dbe28 100644
--- a/src/itdelatrisu/opsu/audio/MusicController.java
+++ b/src/itdelatrisu/opsu/audio/MusicController.java
@@ -20,8 +20,8 @@ package itdelatrisu.opsu.audio;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.OsuParser;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
import java.io.File;
import java.io.IOException;
@@ -50,8 +50,8 @@ public class MusicController {
/** The current music track. */
private static Music player;
- /** The last OsuFile passed to play(). */
- private static OsuFile lastOsu;
+ /** The last beatmap passed to play(). */
+ private static Beatmap lastBeatmap;
/** The track duration. */
private static int duration = 0;
@@ -80,23 +80,23 @@ public class MusicController {
/**
* Plays an audio file at the preview position.
* If the audio file is already playing, then nothing will happen.
- * @param osu the OsuFile to play
+ * @param beatmap the beatmap to play
* @param loop whether or not to loop the track
* @param preview whether to start at the preview time (true) or beginning (false)
*/
- public static void play(final OsuFile osu, final boolean loop, final boolean preview) {
+ public static void play(final Beatmap beatmap, final boolean loop, final boolean preview) {
// new track: load and play
- if (lastOsu == null || !osu.audioFilename.equals(lastOsu.audioFilename)) {
+ if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
reset();
System.gc();
- switch (OsuParser.getExtension(osu.audioFilename.getName())) {
+ switch (BeatmapParser.getExtension(beatmap.audioFilename.getName())) {
case "ogg":
case "mp3":
trackLoader = new Thread() {
@Override
public void run() {
- loadTrack(osu.audioFilename, (preview) ? osu.previewTime : 0, loop);
+ loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
}
};
trackLoader.start();
@@ -107,10 +107,10 @@ public class MusicController {
}
// new track position: play at position
- else if (osu.previewTime != lastOsu.previewTime)
- playAt(osu.previewTime, loop);
+ else if (beatmap.previewTime != lastBeatmap.previewTime)
+ playAt(beatmap.previewTime, loop);
- lastOsu = osu;
+ lastBeatmap = beatmap;
}
/**
@@ -170,9 +170,9 @@ public class MusicController {
public static boolean trackExists() { return (player != null); }
/**
- * Returns the OsuFile associated with the current track.
+ * Returns the beatmap associated with the current track.
*/
- public static OsuFile getOsuFile() { return lastOsu; }
+ public static Beatmap getBeatmap() { return lastBeatmap; }
/**
* Returns true if the current track is playing.
@@ -228,7 +228,7 @@ public class MusicController {
}
/**
- * Returns the position in the current track, in ms.
+ * Returns the position in the current track, in milliseconds.
* If no track is loaded, 0 will be returned.
*/
public static int getPosition() {
@@ -242,6 +242,7 @@ public class MusicController {
/**
* Seeks to a position in the current track.
+ * @param position the new track position (in ms)
*/
public static boolean setPosition(int position) {
return (trackExists() && position >= 0 && player.setPosition(position / 1000f));
@@ -251,17 +252,18 @@ public class MusicController {
* Returns the duration of the current track, in milliseconds.
* Currently only works for MP3s.
* @return the duration, or -1 if no track exists, else the {@code endTime}
- * field of the OsuFile loaded
+ * field of the beatmap loaded
* @author Tom Brito (http://stackoverflow.com/a/3056161)
*/
public static int getDuration() {
- if (!trackExists() || lastOsu == null)
+ if (!trackExists() || lastBeatmap == null)
return -1;
if (duration == 0) {
- if (lastOsu.audioFilename.getName().endsWith(".mp3")) {
+ // TAudioFileFormat method only works for MP3s
+ if (lastBeatmap.audioFilename.getName().endsWith(".mp3")) {
try {
- AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(lastOsu.audioFilename);
+ AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(lastBeatmap.audioFilename);
if (fileFormat instanceof TAudioFileFormat) {
Map, ?> properties = ((TAudioFileFormat) fileFormat).properties();
Long microseconds = (Long) properties.get("duration");
@@ -270,7 +272,9 @@ public class MusicController {
}
} catch (UnsupportedAudioFileException | IOException e) {}
}
- duration = lastOsu.endTime;
+
+ // fallback: use beatmap end time (often not the track duration)
+ duration = lastBeatmap.endTime;
}
return duration;
}
@@ -291,12 +295,20 @@ public class MusicController {
/**
* Sets the music volume.
- * @param volume [0, 1]
+ * @param volume the new volume [0, 1]
*/
public static void setVolume(float volume) {
SoundStore.get().setMusicVolume((isTrackDimmed()) ? volume * dimLevel : volume);
}
+ /**
+ * Sets the music pitch (and speed).
+ * @param pitch the new pitch
+ */
+ public static void setPitch(float pitch) {
+ SoundStore.get().setMusicPitch(pitch);
+ }
+
/**
* Returns whether or not the current track has ended.
*/
@@ -308,16 +320,16 @@ public class MusicController {
*/
public static void loopTrackIfEnded(boolean preview) {
if (trackEnded && trackExists())
- playAt((preview) ? lastOsu.previewTime : 0, false);
+ playAt((preview) ? lastBeatmap.previewTime : 0, false);
}
/**
* Plays the theme song.
*/
public static void playThemeSong() {
- OsuFile osu = Options.getOsuTheme();
- if (osu != null) {
- play(osu, true, false);
+ Beatmap beatmap = Options.getThemeBeatmap();
+ if (beatmap != null) {
+ play(beatmap, true, false);
themePlaying = true;
}
}
@@ -368,7 +380,7 @@ public class MusicController {
trackLoader = null;
// reset state
- lastOsu = null;
+ lastBeatmap = null;
duration = 0;
trackEnded = false;
themePlaying = false;
diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java
index 96c5df26..b91db34e 100644
--- a/src/itdelatrisu/opsu/audio/SoundController.java
+++ b/src/itdelatrisu/opsu/audio/SoundController.java
@@ -20,8 +20,8 @@ package itdelatrisu.opsu.audio;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.audio.HitSound.SampleSet;
+import itdelatrisu.opsu.beatmap.HitObject;
import java.io.File;
import java.io.IOException;
@@ -164,7 +164,7 @@ public class SoundController {
}
if (bestIndex >= 0)
return new MultiClip(ref, AudioSystem.getAudioInputStream(formats[bestIndex], audioIn));
-
+
// still couldn't find anything, try the default clip format
return new MultiClip(ref, AudioSystem.getAudioInputStream(clip.getFormat(), audioIn));
}
@@ -177,11 +177,14 @@ public class SoundController {
*/
private static String getSoundFileName(String filename) {
String wav = String.format("%s.wav", filename), mp3 = String.format("%s.mp3", filename);
- File skinWAV = new File(Options.getSkinDir(), wav), skinMP3 = new File(Options.getSkinDir(), mp3);
- if (skinWAV.isFile())
- return skinWAV.getAbsolutePath();
- if (skinMP3.isFile())
- return skinMP3.getAbsolutePath();
+ File skinDir = Options.getSkin().getDirectory();
+ if (skinDir != null) {
+ File skinWAV = new File(skinDir, wav), skinMP3 = new File(skinDir, mp3);
+ if (skinWAV.isFile())
+ return skinWAV.getAbsolutePath();
+ if (skinMP3.isFile())
+ return skinMP3.getAbsolutePath();
+ }
if (ResourceLoader.resourceExists(wav))
return wav;
if (ResourceLoader.resourceExists(mp3))
@@ -204,7 +207,14 @@ public class SoundController {
ErrorHandler.error(String.format("Could not find sound file '%s'.", s.getFileName()), null, false);
continue;
}
- s.setClip(loadClip(currentFileName, currentFileName.endsWith(".mp3")));
+ MultiClip newClip = loadClip(currentFileName, currentFileName.endsWith(".mp3"));
+ if (s.getClip() != null) { // clip previously loaded (e.g. program restart)
+ if (newClip != null) {
+ s.getClip().destroy(); // destroy previous clip
+ s.setClip(newClip);
+ }
+ } else
+ s.setClip(newClip);
currentFileIndex++;
}
@@ -216,7 +226,14 @@ public class SoundController {
ErrorHandler.error(String.format("Could not find hit sound file '%s'.", filename), null, false);
continue;
}
- s.setClip(ss, loadClip(currentFileName, false));
+ MultiClip newClip = loadClip(currentFileName, false);
+ if (s.getClip(ss) != null) { // clip previously loaded (e.g. program restart)
+ if (newClip != null) {
+ s.getClip(ss).destroy(); // destroy previous clip
+ s.setClip(ss, newClip);
+ }
+ } else
+ s.setClip(ss, newClip);
currentFileIndex++;
}
}
@@ -262,7 +279,7 @@ public class SoundController {
}
/**
- * Plays hit sound(s) using an OsuHitObject bitmask.
+ * Plays hit sound(s) using a HitObject bitmask.
* @param hitSound the hit sound (bitmask)
* @param sampleSet the sample set
* @param additionSampleSet the 'addition' sample set
@@ -276,16 +293,20 @@ public class SoundController {
return;
// play all sounds
- HitSound.setSampleSet(sampleSet);
- playClip(HitSound.NORMAL.getClip(), volume, null);
+ if (hitSound == HitObject.SOUND_NORMAL || Options.getSkin().isLayeredHitSounds()) {
+ HitSound.setSampleSet(sampleSet);
+ playClip(HitSound.NORMAL.getClip(), volume, null);
+ }
- HitSound.setSampleSet(additionSampleSet);
- if ((hitSound & OsuHitObject.SOUND_WHISTLE) > 0)
- playClip(HitSound.WHISTLE.getClip(), volume, null);
- if ((hitSound & OsuHitObject.SOUND_FINISH) > 0)
- playClip(HitSound.FINISH.getClip(), volume, null);
- if ((hitSound & OsuHitObject.SOUND_CLAP) > 0)
- playClip(HitSound.CLAP.getClip(), volume, null);
+ if (hitSound != HitObject.SOUND_NORMAL) {
+ HitSound.setSampleSet(additionSampleSet);
+ if ((hitSound & HitObject.SOUND_WHISTLE) > 0)
+ playClip(HitSound.WHISTLE.getClip(), volume, null);
+ if ((hitSound & HitObject.SOUND_FINISH) > 0)
+ playClip(HitSound.FINISH.getClip(), volume, null);
+ if ((hitSound & HitObject.SOUND_CLAP) > 0)
+ playClip(HitSound.CLAP.getClip(), volume, null);
+ }
}
/**
diff --git a/src/itdelatrisu/opsu/beatmap/Beatmap.java b/src/itdelatrisu/opsu/beatmap/Beatmap.java
new file mode 100644
index 00000000..ad8b5476
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/Beatmap.java
@@ -0,0 +1,455 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.Options;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.util.Log;
+
+/**
+ * Beatmap structure storing data parsed from OSU files.
+ */
+public class Beatmap implements Comparable {
+ /** Game modes. */
+ public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
+
+ /** Background image cache. */
+ private static final BeatmapImageCache bgImageCache = new BeatmapImageCache();
+
+ /**
+ * Returns the background image cache.
+ */
+ public static BeatmapImageCache getBackgroundImageCache() { return bgImageCache; }
+
+ /** The OSU File object associated with this beatmap. */
+ private File file;
+
+ /**
+ * [General]
+ */
+
+ /** Audio file object. */
+ public File audioFilename;
+
+ /** Delay time before music starts (in ms). */
+ public int audioLeadIn = 0;
+
+ /** Audio hash (deprecated). */
+// public String audioHash = "";
+
+ /** Start position of music preview (in ms). */
+ public int previewTime = -1;
+
+ /** Countdown type (0:disabled, 1:normal, 2:half, 3:double). */
+ public byte countdown = 0;
+
+ /** Sound samples ("None", "Normal", "Soft"). */
+ public String sampleSet = "";
+
+ /** How often closely placed hit objects will be stacked together. */
+ public float stackLeniency = 0.7f;
+
+ /** Game mode (MODE_* constants). */
+ public byte mode = MODE_OSU;
+
+ /** Whether the letterbox (top/bottom black bars) appears during breaks. */
+ public boolean letterboxInBreaks = false;
+
+ /** Whether the storyboard should be widescreen. */
+ public boolean widescreenStoryboard = false;
+
+ /** Whether to show an epilepsy warning. */
+ public boolean epilepsyWarning = false;
+
+ /**
+ * [Editor]
+ */
+
+ /** List of editor bookmarks (in ms). */
+// public int[] bookmarks;
+
+ /** Multiplier for "Distance Snap". */
+// public float distanceSpacing = 0f;
+
+ /** Beat division. */
+// public byte beatDivisor = 0;
+
+ /** Size of grid for "Grid Snap". */
+// public int gridSize = 0;
+
+ /** Zoom in the editor timeline. */
+// public int timelineZoom = 0;
+
+ /**
+ * [Metadata]
+ */
+
+ /** Song title. */
+ public String title = "", titleUnicode = "";
+
+ /** Song artist. */
+ public String artist = "", artistUnicode = "";
+
+ /** Beatmap creator. */
+ public String creator = "";
+
+ /** Beatmap difficulty. */
+ public String version = "";
+
+ /** Song source. */
+ public String source = "";
+
+ /** Song tags (for searching). */
+ public String tags = "";
+
+ /** Beatmap ID. */
+ public int beatmapID = 0;
+
+ /** Beatmap set ID. */
+ public int beatmapSetID = 0;
+
+ /**
+ * [Difficulty]
+ */
+
+ /** HP: Health drain rate (0:easy ~ 10:hard) */
+ public float HPDrainRate = 5f;
+
+ /** CS: Size of circles and sliders (0:large ~ 10:small). */
+ public float circleSize = 4f;
+
+ /** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
+ public float overallDifficulty = 5f;
+
+ /** AR: How long circles stay on the screen (0:long ~ 10:short). */
+ public float approachRate = -1f;
+
+ /** Slider movement speed multiplier. */
+ public float sliderMultiplier = 1f;
+
+ /** Rate at which slider ticks are placed (x per beat). */
+ public float sliderTickRate = 1f;
+
+ /**
+ * [Events]
+ */
+
+ /** Background image file. */
+ public File bg;
+
+ /** Background video file. */
+// public File video;
+
+ /** All break periods (start time, end time, ...). */
+ public ArrayList breaks;
+
+ /**
+ * [TimingPoints]
+ */
+
+ /** All timing points. */
+ public ArrayList timingPoints;
+
+ /** Song BPM range. */
+ public int bpmMin = 0, bpmMax = 0;
+
+ /**
+ * [Colours]
+ */
+
+ /** Combo colors (max 8). If null, the skin value is used. */
+ public Color[] combo;
+
+ /** Slider border color. If null, the skin value is used. */
+ public Color sliderBorder;
+
+ /**
+ * [HitObjects]
+ */
+
+ /** All hit objects. */
+ public HitObject[] objects;
+
+ /** Number of individual objects. */
+ public int
+ hitObjectCircle = 0,
+ hitObjectSlider = 0,
+ hitObjectSpinner = 0;
+
+ /** Last object end time (in ms). */
+ public int endTime = -1;
+
+ /**
+ * Constructor.
+ * @param file the file associated with this beatmap
+ */
+ public Beatmap(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Returns the associated file object.
+ * @return the File object
+ */
+ public File getFile() { return file; }
+
+ /**
+ * Returns the song title.
+ * If configured, the Unicode string will be returned instead.
+ * @return the song title
+ */
+ public String getTitle() {
+ return (Options.useUnicodeMetadata() && !titleUnicode.isEmpty()) ? titleUnicode : title;
+ }
+
+ /**
+ * Returns the song artist.
+ * If configured, the Unicode string will be returned instead.
+ * @return the song artist
+ */
+ public String getArtist() {
+ return (Options.useUnicodeMetadata() && !artistUnicode.isEmpty()) ? artistUnicode : artist;
+ }
+
+ /**
+ * Returns the list of combo colors (max 8).
+ * If the beatmap does not provide colors, the skin colors will be returned instead.
+ * @return the combo colors
+ */
+ public Color[] getComboColors() {
+ return (combo != null) ? combo : Options.getSkin().getComboColors();
+ }
+
+ /**
+ * Returns the slider border color.
+ * If the beatmap does not provide a color, the skin color will be returned instead.
+ * @return the slider border color
+ */
+ public Color getSliderBorderColor() {
+ return (sliderBorder != null) ? sliderBorder : Options.getSkin().getSliderBorderColor();
+ }
+
+ /**
+ * Draws the beatmap background.
+ * @param width the container width
+ * @param height the container height
+ * @param alpha the alpha value
+ * @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
+ * @return true if successful, false if any errors were produced
+ */
+ public boolean drawBG(int width, int height, float alpha, boolean stretch) {
+ if (bg == null)
+ return false;
+ try {
+ Image bgImage = bgImageCache.get(this);
+ if (bgImage == null) {
+ bgImage = new Image(bg.getAbsolutePath());
+ bgImageCache.put(this, bgImage);
+ }
+
+ int swidth = width;
+ int sheight = height;
+ if (!stretch) {
+ // fit image to screen
+ if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
+ sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
+ else
+ swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
+ } else {
+ // fill screen while maintaining aspect ratio
+ if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
+ swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
+ else
+ sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
+ }
+ bgImage = bgImage.getScaledCopy(swidth, sheight);
+
+ bgImage.setAlpha(alpha);
+ bgImage.drawCentered(width / 2, height / 2);
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to get background image '%s'.", bg), e);
+ bg = null; // don't try to load the file again until a restart
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Compares two Beatmap objects first by overall difficulty, then by total objects.
+ */
+ @Override
+ public int compareTo(Beatmap that) {
+ int cmp = Float.compare(this.overallDifficulty, that.overallDifficulty);
+ if (cmp == 0)
+ cmp = Integer.compare(
+ this.hitObjectCircle + this.hitObjectSlider + this.hitObjectSpinner,
+ that.hitObjectCircle + that.hitObjectSlider + that.hitObjectSpinner
+ );
+ return cmp;
+ }
+
+ /**
+ * Returns a formatted string: "Artist - Title [Version]"
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return String.format("%s - %s [%s]", getArtist(), getTitle(), version);
+ }
+
+ /**
+ * Returns the {@link #breaks} field formatted as a string,
+ * or null if the field is null.
+ */
+ public String breaksToString() {
+ if (breaks == null)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+ for (int i : breaks) {
+ sb.append(i);
+ sb.append(',');
+ }
+ if (sb.length() > 0)
+ sb.setLength(sb.length() - 1);
+ return sb.toString();
+ }
+
+ /**
+ * Sets the {@link #breaks} field from a string.
+ * @param s the string
+ */
+ public void breaksFromString(String s) {
+ if (s == null)
+ return;
+
+ this.breaks = new ArrayList();
+ String[] tokens = s.split(",");
+ for (int i = 0; i < tokens.length; i++)
+ breaks.add(Integer.parseInt(tokens[i]));
+ }
+
+ /**
+ * Returns the {@link #timingPoints} field formatted as a string,
+ * or null if the field is null.
+ */
+ public String timingPointsToString() {
+ if (timingPoints == null)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+ for (TimingPoint p : timingPoints) {
+ sb.append(p.toString());
+ sb.append('|');
+ }
+ if (sb.length() > 0)
+ sb.setLength(sb.length() - 1);
+ return sb.toString();
+ }
+
+ /**
+ * Sets the {@link #timingPoints} field from a string.
+ * @param s the string
+ */
+ public void timingPointsFromString(String s) {
+ this.timingPoints = new ArrayList();
+ if (s == null)
+ return;
+
+ String[] tokens = s.split("\\|");
+ for (int i = 0; i < tokens.length; i++) {
+ try {
+ timingPoints.add(new TimingPoint(tokens[i]));
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read timing point '%s'.", tokens[i]), e);
+ }
+ }
+ timingPoints.trimToSize();
+ }
+
+ /**
+ * Returns the {@link #combo} field formatted as a string,
+ * or null if the field is null or the default combo.
+ */
+ public String comboToString() {
+ if (combo == null)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < combo.length; i++) {
+ Color c = combo[i];
+ sb.append(c.getRed());
+ sb.append(',');
+ sb.append(c.getGreen());
+ sb.append(',');
+ sb.append(c.getBlue());
+ sb.append('|');
+ }
+ if (sb.length() > 0)
+ sb.setLength(sb.length() - 1);
+ return sb.toString();
+ }
+
+ /**
+ * Sets the {@link #combo} field from a string.
+ * @param s the string
+ */
+ public void comboFromString(String s) {
+ if (s == null)
+ return;
+
+ LinkedList colors = new LinkedList();
+ String[] tokens = s.split("\\|");
+ for (int i = 0; i < tokens.length; i++) {
+ String[] rgb = tokens[i].split(",");
+ colors.add(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
+ }
+ if (!colors.isEmpty())
+ this.combo = colors.toArray(new Color[colors.size()]);
+ }
+
+ /**
+ * Returns the {@link #sliderBorder} field formatted as a string,
+ * or null if the field is null.
+ */
+ public String sliderBorderToString() {
+ if (sliderBorder == null)
+ return null;
+
+ return String.format("%d,%d,%d", sliderBorder.getRed(), sliderBorder.getGreen(), sliderBorder.getBlue());
+ }
+
+ /**
+ * Sets the {@link #sliderBorder} field from a string.
+ * @param s the string
+ */
+ public void sliderBorderFromString(String s) {
+ if (s == null)
+ return;
+
+ String[] rgb = s.split(",");
+ this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
+ }
+}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java b/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java
new file mode 100644
index 00000000..fdf502f9
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java
@@ -0,0 +1,86 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.newdawn.slick.Image;
+import org.newdawn.slick.SlickException;
+import org.newdawn.slick.util.Log;
+
+/**
+ * LRU cache for beatmap background images.
+ */
+public class BeatmapImageCache {
+ /** Maximum number of cached images. */
+ private static final int MAX_CACHE_SIZE = 10;
+
+ /** Map of all loaded background images. */
+ private LinkedHashMap cache;
+
+ /**
+ * Constructor.
+ */
+ @SuppressWarnings("serial")
+ public BeatmapImageCache() {
+ this.cache = new LinkedHashMap(MAX_CACHE_SIZE + 1, 1.1f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ if (size() > MAX_CACHE_SIZE) {
+ // destroy the eldest image
+ Image img = eldest.getValue();
+ if (img != null && !img.isDestroyed()) {
+ try {
+ img.destroy();
+ } catch (SlickException e) {
+ Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Returns the image mapped to the specified beatmap.
+ * @param beatmap the Beatmap
+ * @return the Image, or {@code null} if no such mapping exists
+ */
+ public Image get(Beatmap beatmap) { return cache.get(beatmap.bg); }
+
+ /**
+ * Creates a mapping from the specified beatmap to the given image.
+ * @param beatmap the Beatmap
+ * @param image the Image
+ * @return the previously mapped Image, or {@code null} if no such mapping existed
+ */
+ public Image put(Beatmap beatmap, Image image) { return cache.put(beatmap.bg, image); }
+
+ /**
+ * Removes all entries from the cache.
+ *
+ * NOTE: This does NOT destroy the images in the cache, and will cause
+ * memory leaks if all images have not been destroyed.
+ */
+ public void clear() { cache.clear(); }
+}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java
new file mode 100644
index 00000000..369ea19e
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java
@@ -0,0 +1,741 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.db.BeatmapDB;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.util.Log;
+
+/**
+ * Parser for beatmaps.
+ */
+public class BeatmapParser {
+ /** The string lookup database. */
+ private static HashMap stringdb = new HashMap();
+
+ /** The expected pattern for beatmap directories, used to find beatmap set IDs. */
+ private static final String DIR_MSID_PATTERN = "^\\d+ .*";
+
+ /** The current file being parsed. */
+ private static File currentFile;
+
+ /** The current directory number while parsing. */
+ private static int currentDirectoryIndex = -1;
+
+ /** The total number of directories to parse. */
+ private static int totalDirectories = -1;
+
+ /** Parser statuses. */
+ public enum Status { NONE, PARSING, CACHE, INSERTING };
+
+ /** The current status. */
+ private static Status status = Status.NONE;
+
+ // This class should not be instantiated.
+ private BeatmapParser() {}
+
+ /**
+ * Invokes parser for each OSU file in a root directory and
+ * adds the beatmaps to a new BeatmapSetList.
+ * @param root the root directory (search has depth 1)
+ */
+ public static void parseAllFiles(File root) {
+ // create a new BeatmapSetList
+ BeatmapSetList.create();
+
+ // parse all directories
+ parseDirectories(root.listFiles());
+ }
+
+ /**
+ * Invokes parser for each directory in the given array and
+ * adds the beatmaps to the existing BeatmapSetList.
+ * @param dirs the array of directories to parse
+ * @return the last BeatmapSetNode parsed, or null if none
+ */
+ public static BeatmapSetNode parseDirectories(File[] dirs) {
+ if (dirs == null)
+ return null;
+
+ // progress tracking
+ status = Status.PARSING;
+ currentDirectoryIndex = 0;
+ totalDirectories = dirs.length;
+
+ // get last modified map from database
+ Map map = BeatmapDB.getLastModifiedMap();
+
+ // beatmap lists
+ List> allBeatmaps = new LinkedList>();
+ List cachedBeatmaps = new LinkedList(); // loaded from database
+ List parsedBeatmaps = new LinkedList(); // loaded from parser
+
+ // parse directories
+ BeatmapSetNode lastNode = null;
+ for (File dir : dirs) {
+ currentDirectoryIndex++;
+ if (!dir.isDirectory())
+ continue;
+
+ // find all OSU files
+ File[] files = dir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".osu");
+ }
+ });
+ if (files.length < 1)
+ continue;
+
+ // create a new group entry
+ ArrayList beatmaps = new ArrayList();
+ for (File file : files) {
+ currentFile = file;
+
+ // check if beatmap is cached
+ String path = String.format("%s/%s", dir.getName(), file.getName());
+ if (map.containsKey(path)) {
+ // check last modified times
+ long lastModified = map.get(path);
+ if (lastModified == file.lastModified()) {
+ // add to cached beatmap list
+ Beatmap beatmap = new Beatmap(file);
+ beatmaps.add(beatmap);
+ cachedBeatmaps.add(beatmap);
+ continue;
+ } else
+ BeatmapDB.delete(dir.getName(), file.getName());
+ }
+
+ // Parse hit objects only when needed to save time/memory.
+ // Change boolean to 'true' to parse them immediately.
+ Beatmap beatmap = parseFile(file, dir, beatmaps, false);
+
+ // add to parsed beatmap list
+ if (beatmap != null) {
+ beatmaps.add(beatmap);
+ parsedBeatmaps.add(beatmap);
+ }
+ }
+
+ // add group entry if non-empty
+ if (!beatmaps.isEmpty()) {
+ beatmaps.trimToSize();
+ allBeatmaps.add(beatmaps);
+ }
+
+ // stop parsing files (interrupted)
+ if (Thread.interrupted())
+ break;
+ }
+
+ // load cached entries from database
+ if (!cachedBeatmaps.isEmpty()) {
+ status = Status.CACHE;
+
+ // Load array fields only when needed to save time/memory.
+ // Change flag to 'LOAD_ALL' to load them immediately.
+ BeatmapDB.load(cachedBeatmaps, BeatmapDB.LOAD_NONARRAY);
+ }
+
+ // add group entries to BeatmapSetList
+ for (ArrayList beatmaps : allBeatmaps) {
+ Collections.sort(beatmaps);
+ lastNode = BeatmapSetList.get().addSongGroup(beatmaps);
+ }
+
+ // clear string DB
+ stringdb = new HashMap();
+
+ // add beatmap entries to database
+ if (!parsedBeatmaps.isEmpty()) {
+ status = Status.INSERTING;
+ BeatmapDB.insert(parsedBeatmaps);
+ }
+
+ status = Status.NONE;
+ currentFile = null;
+ currentDirectoryIndex = -1;
+ totalDirectories = -1;
+ return lastNode;
+ }
+
+ /**
+ * Parses a beatmap.
+ * @param file the file to parse
+ * @param dir the directory containing the beatmap
+ * @param beatmaps the song group
+ * @param parseObjects if true, hit objects will be fully parsed now
+ * @return the new beatmap
+ */
+ private static Beatmap parseFile(File file, File dir, ArrayList beatmaps, boolean parseObjects) {
+ Beatmap beatmap = new Beatmap(file);
+ beatmap.timingPoints = new ArrayList();
+
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
+ String line = in.readLine();
+ String tokens[] = null;
+ while (line != null) {
+ line = line.trim();
+ if (!isValidLine(line)) {
+ line = in.readLine();
+ continue;
+ }
+ switch (line) {
+ case "[General]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ switch (tokens[0]) {
+ case "AudioFilename":
+ File audioFileName = new File(dir, tokens[1]);
+ if (!beatmaps.isEmpty()) {
+ // if possible, reuse the same File object from another Beatmap in the group
+ File groupAudioFileName = beatmaps.get(0).audioFilename;
+ if (groupAudioFileName != null &&
+ tokens[1].equalsIgnoreCase(groupAudioFileName.getName()))
+ audioFileName = groupAudioFileName;
+ }
+ if (!audioFileName.isFile()) {
+ // try to find the file with a case-insensitive match
+ boolean match = false;
+ for (String s : dir.list()) {
+ if (s.equalsIgnoreCase(tokens[1])) {
+ audioFileName = new File(dir, s);
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ Log.error(String.format("Audio file '%s' not found in directory '%s'.", tokens[1], dir.getName()));
+ return null;
+ }
+ }
+ beatmap.audioFilename = audioFileName;
+ break;
+ case "AudioLeadIn":
+ beatmap.audioLeadIn = Integer.parseInt(tokens[1]);
+ break;
+// case "AudioHash": // deprecated
+// beatmap.audioHash = tokens[1];
+// break;
+ case "PreviewTime":
+ beatmap.previewTime = Integer.parseInt(tokens[1]);
+ break;
+ case "Countdown":
+ beatmap.countdown = Byte.parseByte(tokens[1]);
+ break;
+ case "SampleSet":
+ beatmap.sampleSet = getDBString(tokens[1]);
+ break;
+ case "StackLeniency":
+ beatmap.stackLeniency = Float.parseFloat(tokens[1]);
+ break;
+ case "Mode":
+ beatmap.mode = Byte.parseByte(tokens[1]);
+
+ /* Non-Opsu! standard files not implemented (obviously). */
+ if (beatmap.mode != Beatmap.MODE_OSU)
+ return null;
+
+ break;
+ case "LetterboxInBreaks":
+ beatmap.letterboxInBreaks = Utils.parseBoolean(tokens[1]);
+ break;
+ case "WidescreenStoryboard":
+ beatmap.widescreenStoryboard = Utils.parseBoolean(tokens[1]);
+ break;
+ case "EpilepsyWarning":
+ beatmap.epilepsyWarning = Utils.parseBoolean(tokens[1]);
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read line '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ }
+ break;
+ case "[Editor]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ /* Not implemented. */
+// if ((tokens = tokenize(line)) == null)
+// continue;
+// try {
+// switch (tokens[0]) {
+// case "Bookmarks":
+// String[] bookmarks = tokens[1].split(",");
+// beatmap.bookmarks = new int[bookmarks.length];
+// for (int i = 0; i < bookmarks.length; i++)
+// osu.bookmarks[i] = Integer.parseInt(bookmarks[i]);
+// break;
+// case "DistanceSpacing":
+// beatmap.distanceSpacing = Float.parseFloat(tokens[1]);
+// break;
+// case "BeatDivisor":
+// beatmap.beatDivisor = Byte.parseByte(tokens[1]);
+// break;
+// case "GridSize":
+// beatmap.gridSize = Integer.parseInt(tokens[1]);
+// break;
+// case "TimelineZoom":
+// beatmap.timelineZoom = Integer.parseInt(tokens[1]);
+// break;
+// default:
+// break;
+// }
+// } catch (Exception e) {
+// Log.warn(String.format("Failed to read editor line '%s' for file '%s'.",
+// line, file.getAbsolutePath()), e);
+// }
+ }
+ break;
+ case "[Metadata]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ switch (tokens[0]) {
+ case "Title":
+ beatmap.title = getDBString(tokens[1]);
+ break;
+ case "TitleUnicode":
+ beatmap.titleUnicode = getDBString(tokens[1]);
+ break;
+ case "Artist":
+ beatmap.artist = getDBString(tokens[1]);
+ break;
+ case "ArtistUnicode":
+ beatmap.artistUnicode = getDBString(tokens[1]);
+ break;
+ case "Creator":
+ beatmap.creator = getDBString(tokens[1]);
+ break;
+ case "Version":
+ beatmap.version = getDBString(tokens[1]);
+ break;
+ case "Source":
+ beatmap.source = getDBString(tokens[1]);
+ break;
+ case "Tags":
+ beatmap.tags = getDBString(tokens[1].toLowerCase());
+ break;
+ case "BeatmapID":
+ beatmap.beatmapID = Integer.parseInt(tokens[1]);
+ break;
+ case "BeatmapSetID":
+ beatmap.beatmapSetID = Integer.parseInt(tokens[1]);
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read metadata '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ if (beatmap.beatmapSetID <= 0) { // try to determine MSID from directory name
+ if (dir != null && dir.isDirectory()) {
+ String dirName = dir.getName();
+ if (!dirName.isEmpty() && dirName.matches(DIR_MSID_PATTERN))
+ beatmap.beatmapSetID = Integer.parseInt(dirName.substring(0, dirName.indexOf(' ')));
+ }
+ }
+ }
+ break;
+ case "[Difficulty]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ switch (tokens[0]) {
+ case "HPDrainRate":
+ beatmap.HPDrainRate = Float.parseFloat(tokens[1]);
+ break;
+ case "CircleSize":
+ beatmap.circleSize = Float.parseFloat(tokens[1]);
+ break;
+ case "OverallDifficulty":
+ beatmap.overallDifficulty = Float.parseFloat(tokens[1]);
+ break;
+ case "ApproachRate":
+ beatmap.approachRate = Float.parseFloat(tokens[1]);
+ break;
+ case "SliderMultiplier":
+ beatmap.sliderMultiplier = Float.parseFloat(tokens[1]);
+ break;
+ case "SliderTickRate":
+ beatmap.sliderTickRate = Float.parseFloat(tokens[1]);
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read difficulty '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ }
+ if (beatmap.approachRate == -1f) // not in old format
+ beatmap.approachRate = beatmap.overallDifficulty;
+ break;
+ case "[Events]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ tokens = line.split(",");
+ switch (tokens[0]) {
+ case "0": // background
+ tokens[2] = tokens[2].replaceAll("^\"|\"$", "");
+ String ext = BeatmapParser.getExtension(tokens[2]);
+ if (ext.equals("jpg") || ext.equals("png"))
+ beatmap.bg = new File(dir, getDBString(tokens[2]));
+ break;
+ case "2": // break periods
+ try {
+ if (beatmap.breaks == null) // optional, create if needed
+ beatmap.breaks = new ArrayList();
+ beatmap.breaks.add(Integer.parseInt(tokens[1]));
+ beatmap.breaks.add(Integer.parseInt(tokens[2]));
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read break period '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ break;
+ default:
+ /* Not implemented. */
+ break;
+ }
+ }
+ if (beatmap.breaks != null)
+ beatmap.breaks.trimToSize();
+ break;
+ case "[TimingPoints]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+
+ try {
+ // parse timing point
+ TimingPoint timingPoint = new TimingPoint(line);
+
+ // calculate BPM
+ if (!timingPoint.isInherited()) {
+ int bpm = Math.round(60000 / timingPoint.getBeatLength());
+ if (beatmap.bpmMin == 0)
+ beatmap.bpmMin = beatmap.bpmMax = bpm;
+ else if (bpm < beatmap.bpmMin)
+ beatmap.bpmMin = bpm;
+ else if (bpm > beatmap.bpmMax)
+ beatmap.bpmMax = bpm;
+ }
+
+ beatmap.timingPoints.add(timingPoint);
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ }
+ beatmap.timingPoints.trimToSize();
+ break;
+ case "[Colours]":
+ LinkedList colors = new LinkedList();
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ String[] rgb = tokens[1].split(",");
+ Color color = new Color(
+ Integer.parseInt(rgb[0]),
+ Integer.parseInt(rgb[1]),
+ Integer.parseInt(rgb[2])
+ );
+ switch (tokens[0]) {
+ case "Combo1":
+ case "Combo2":
+ case "Combo3":
+ case "Combo4":
+ case "Combo5":
+ case "Combo6":
+ case "Combo7":
+ case "Combo8":
+ colors.add(color);
+ break;
+ case "SliderBorder":
+ beatmap.sliderBorder = color;
+ break;
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read color '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ }
+ if (!colors.isEmpty())
+ beatmap.combo = colors.toArray(new Color[colors.size()]);
+ break;
+ case "[HitObjects]":
+ int type = 0;
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ /* Only type counts parsed at this time. */
+ tokens = line.split(",");
+ try {
+ type = Integer.parseInt(tokens[3]);
+ if ((type & HitObject.TYPE_CIRCLE) > 0)
+ beatmap.hitObjectCircle++;
+ else if ((type & HitObject.TYPE_SLIDER) > 0)
+ beatmap.hitObjectSlider++;
+ else //if ((type & HitObject.TYPE_SPINNER) > 0)
+ beatmap.hitObjectSpinner++;
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read hit object '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ }
+
+ try {
+ // map length = last object end time (TODO: end on slider?)
+ if ((type & HitObject.TYPE_SPINNER) > 0) {
+ // some 'endTime' fields contain a ':' character (?)
+ int index = tokens[5].indexOf(':');
+ if (index != -1)
+ tokens[5] = tokens[5].substring(0, index);
+ beatmap.endTime = Integer.parseInt(tokens[5]);
+ } else if (type != 0)
+ beatmap.endTime = Integer.parseInt(tokens[2]);
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read hit object end time '%s' for file '%s'.",
+ line, file.getAbsolutePath()), e);
+ }
+ break;
+ default:
+ line = in.readLine();
+ break;
+ }
+ }
+ } catch (IOException e) {
+ ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false);
+ }
+
+ // no associated audio file?
+ if (beatmap.audioFilename == null)
+ return null;
+
+ // parse hit objects now?
+ if (parseObjects)
+ parseHitObjects(beatmap);
+
+ return beatmap;
+ }
+
+ /**
+ * Parses all hit objects in a beatmap.
+ * @param beatmap the beatmap to parse
+ */
+ public static void parseHitObjects(Beatmap beatmap) {
+ if (beatmap.objects != null) // already parsed
+ return;
+
+ beatmap.objects = new HitObject[(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner)];
+
+ try (BufferedReader in = new BufferedReader(new FileReader(beatmap.getFile()))) {
+ String line = in.readLine();
+ while (line != null) {
+ line = line.trim();
+ if (!line.equals("[HitObjects]"))
+ line = in.readLine();
+ else
+ break;
+ }
+ if (line == null) {
+ Log.warn(String.format("No hit objects found in Beatmap '%s'.", beatmap.toString()));
+ return;
+ }
+
+ // combo info
+ Color[] combo = beatmap.getComboColors();
+ int comboIndex = 0; // color index
+ int comboNumber = 1; // combo number
+
+ int objectIndex = 0;
+ boolean first = true;
+ while ((line = in.readLine()) != null && objectIndex < beatmap.objects.length) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+
+ // lines must have at minimum 5 parameters
+ int tokenCount = line.length() - line.replace(",", "").length();
+ if (tokenCount < 4)
+ continue;
+
+ try {
+ // create a new HitObject for each line
+ HitObject hitObject = new HitObject(line);
+
+ // set combo info
+ // - new combo: get next combo index, reset combo number
+ // - else: maintain combo index, increase combo number
+ if (hitObject.isNewCombo() || first) {
+ int skip = (hitObject.isSpinner() ? 0 : 1) + hitObject.getComboSkip();
+ for (int i = 0; i < skip; i++) {
+ comboIndex = (comboIndex + 1) % combo.length;
+ comboNumber = 1;
+ }
+ first = false;
+ }
+
+ hitObject.setComboIndex(comboIndex);
+ hitObject.setComboNumber(comboNumber++);
+
+ beatmap.objects[objectIndex++] = hitObject;
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read hit object '%s' for Beatmap '%s'.",
+ line, beatmap.toString()), e);
+ }
+ }
+ } catch (IOException e) {
+ ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
+ }
+ }
+
+ /**
+ * Returns false if the line is too short or commented.
+ */
+ private static boolean isValidLine(String line) {
+ return (line.length() > 1 && !line.startsWith("//"));
+ }
+
+ /**
+ * Splits line into two strings: tag, value.
+ * If no ':' character is present, null will be returned.
+ */
+ private static String[] tokenize(String line) {
+ int index = line.indexOf(':');
+ if (index == -1) {
+ Log.debug(String.format("Failed to tokenize line: '%s'.", line));
+ return null;
+ }
+
+ String[] tokens = new String[2];
+ tokens[0] = line.substring(0, index).trim();
+ tokens[1] = line.substring(index + 1).trim();
+ return tokens;
+ }
+
+ /**
+ * Returns the file extension of a file.
+ */
+ public static String getExtension(String file) {
+ int i = file.lastIndexOf('.');
+ return (i != -1) ? file.substring(i + 1).toLowerCase() : "";
+ }
+
+ /**
+ * Returns the name of the current file being parsed, or null if none.
+ */
+ public static String getCurrentFileName() {
+ if (status == Status.PARSING)
+ return (currentFile != null) ? currentFile.getName() : null;
+ else
+ return (status == Status.NONE) ? null : "";
+ }
+
+ /**
+ * Returns the progress of file parsing, or -1 if not parsing.
+ * @return the completion percent [0, 100] or -1
+ */
+ public static int getParserProgress() {
+ if (currentDirectoryIndex == -1 || totalDirectories == -1)
+ return -1;
+
+ return currentDirectoryIndex * 100 / totalDirectories;
+ }
+
+ /**
+ * Returns the current parser status.
+ */
+ public static Status getStatus() { return status; }
+
+ /**
+ * Returns the String object in the database for the given String.
+ * If none, insert the String into the database and return the original String.
+ * @param s the string to retrieve
+ * @return the string object
+ */
+ public static String getDBString(String s) {
+ String DBString = stringdb.get(s);
+ if (DBString == null) {
+ stringdb.put(s, s);
+ return s;
+ } else
+ return DBString;
+ }
+}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSet.java b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java
new file mode 100644
index 00000000..8ba0693b
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java
@@ -0,0 +1,178 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.GameMod;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type containing all beatmaps in a beatmap set.
+ */
+public class BeatmapSet {
+ /** List of associated beatmaps. */
+ private ArrayList beatmaps;
+
+ /**
+ * Constructor.
+ * @param beatmaps the beatmaps in this set
+ */
+ public BeatmapSet(ArrayList beatmaps) {
+ this.beatmaps = beatmaps;
+ }
+
+ /**
+ * Returns the number of elements.
+ */
+ public int size() { return beatmaps.size(); }
+
+ /**
+ * Returns the beatmap at the given index.
+ * @param index the beatmap index
+ * @throws IndexOutOfBoundsException
+ */
+ public Beatmap get(int index) { return beatmaps.get(index); }
+
+ /**
+ * Removes the beatmap at the given index.
+ * @param index the beatmap index
+ * @return the removed beatmap
+ * @throws IndexOutOfBoundsException
+ */
+ public Beatmap remove(int index) { return beatmaps.remove(index); }
+
+ /**
+ * Returns an array of strings containing beatmap information.
+ *
+ * - 0: {Artist} - {Title} [{Version}]
+ *
- 1: Mapped by {Creator}
+ *
- 2: Length: {} BPM: {} Objects: {}
+ *
- 3: Circles: {} Sliders: {} Spinners: {}
+ *
- 4: CS:{} HP:{} AR:{} OD:{}
+ *
+ * @param index the beatmap index
+ * @throws IndexOutOfBoundsException
+ */
+ public String[] getInfo(int index) {
+ Beatmap beatmap = beatmaps.get(index);
+ float speedModifier = GameMod.getSpeedMultiplier();
+ long endTime = (long) (beatmap.endTime / speedModifier);
+ int bpmMin = (int) (beatmap.bpmMin * speedModifier);
+ int bpmMax = (int) (beatmap.bpmMax * speedModifier);
+ float multiplier = GameMod.getDifficultyMultiplier();
+ String[] info = new String[5];
+ info[0] = beatmap.toString();
+ info[1] = String.format("Mapped by %s", beatmap.creator);
+ info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d",
+ TimeUnit.MILLISECONDS.toMinutes(endTime),
+ TimeUnit.MILLISECONDS.toSeconds(endTime) -
+ TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(endTime)),
+ (bpmMax <= 0) ? "--" : ((bpmMin == bpmMax) ? bpmMin : String.format("%d-%d", bpmMin, bpmMax)),
+ (beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
+ info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
+ beatmap.hitObjectCircle, beatmap.hitObjectSlider, beatmap.hitObjectSpinner);
+ info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f",
+ Math.min(beatmap.circleSize * multiplier, 10f),
+ Math.min(beatmap.HPDrainRate * multiplier, 10f),
+ Math.min(beatmap.approachRate * multiplier, 10f),
+ Math.min(beatmap.overallDifficulty * multiplier, 10f));
+ return info;
+ }
+
+ /**
+ * Returns a formatted string for the beatmap set:
+ * "Artist - Title"
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ Beatmap beatmap = beatmaps.get(0);
+ return String.format("%s - %s", beatmap.getArtist(), beatmap.getTitle());
+ }
+
+ /**
+ * Checks whether the beatmap set matches a given search query.
+ * @param query the search term
+ * @return true if title, artist, creator, source, version, or tag matches query
+ */
+ public boolean matches(String query) {
+ // search: title, artist, creator, source, version, tags (first beatmap)
+ Beatmap beatmap = beatmaps.get(0);
+ if (beatmap.title.toLowerCase().contains(query) ||
+ beatmap.titleUnicode.toLowerCase().contains(query) ||
+ beatmap.artist.toLowerCase().contains(query) ||
+ beatmap.artistUnicode.toLowerCase().contains(query) ||
+ beatmap.creator.toLowerCase().contains(query) ||
+ beatmap.source.toLowerCase().contains(query) ||
+ beatmap.version.toLowerCase().contains(query) ||
+ beatmap.tags.contains(query))
+ return true;
+
+ // search: version, tags (remaining beatmaps)
+ for (int i = 1; i < beatmaps.size(); i++) {
+ beatmap = beatmaps.get(i);
+ if (beatmap.version.toLowerCase().contains(query) ||
+ beatmap.tags.contains(query))
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the beatmap set matches a given condition.
+ * @param type the condition type (ar, cs, od, hp, bpm, length)
+ * @param operator the operator {@literal (=/==, >, >=, <, <=)}
+ * @param value the value
+ * @return true if the condition is met
+ */
+ public boolean matches(String type, String operator, float value) {
+ for (Beatmap beatmap : beatmaps) {
+ // get value
+ float v;
+ switch (type) {
+ case "ar": v = beatmap.approachRate; break;
+ case "cs": v = beatmap.circleSize; break;
+ case "od": v = beatmap.overallDifficulty; break;
+ case "hp": v = beatmap.HPDrainRate; break;
+ case "bpm": v = beatmap.bpmMax; break;
+ case "length": v = beatmap.endTime / 1000; break;
+ default: return false;
+ }
+
+ // get operator
+ boolean met;
+ switch (operator) {
+ case "=":
+ case "==": met = (v == value); break;
+ case ">": met = (v > value); break;
+ case ">=": met = (v >= value); break;
+ case "<": met = (v < value); break;
+ case "<=": met = (v <= value); break;
+ default: return false;
+ }
+
+ if (met)
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
new file mode 100644
index 00000000..7c5aab81
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
@@ -0,0 +1,504 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.audio.MusicController;
+import itdelatrisu.opsu.db.BeatmapDB;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Indexed, expanding, doubly-linked list data type for song groups.
+ */
+public class BeatmapSetList {
+ /** Song group structure (each group contains a list of beatmaps). */
+ private static BeatmapSetList list;
+
+ /** Search pattern for conditional expressions. */
+ private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
+ "(ar|cs|od|hp|bpm|length)(=|==|>|>=|<|<=)((\\d*\\.)?\\d+)"
+ );
+
+ /** List containing all parsed nodes. */
+ private ArrayList parsedNodes;
+
+ /** Total number of beatmaps (i.e. Beatmap objects). */
+ private int mapCount = 0;
+
+ /** Current list of nodes (subset of parsedNodes, used for searches). */
+ private ArrayList nodes;
+
+ /** Set of all beatmap set IDs for the parsed beatmaps. */
+ private HashSet MSIDdb;
+
+ /** Index of current expanded node (-1 if no node is expanded). */
+ private int expandedIndex;
+
+ /** Start and end nodes of expanded group. */
+ private BeatmapSetNode expandedStartNode, expandedEndNode;
+
+ /** The last search query. */
+ private String lastQuery;
+
+ /**
+ * Creates a new instance of this class (overwriting any previous instance).
+ */
+ public static void create() { list = new BeatmapSetList(); }
+
+ /**
+ * Returns the single instance of this class.
+ */
+ public static BeatmapSetList get() { return list; }
+
+ /**
+ * Constructor.
+ */
+ private BeatmapSetList() {
+ parsedNodes = new ArrayList();
+ MSIDdb = new HashSet();
+ reset();
+ }
+
+ /**
+ * Resets the list's fields.
+ * This does not erase any parsed nodes.
+ */
+ public void reset() {
+ nodes = parsedNodes;
+ expandedIndex = -1;
+ expandedStartNode = expandedEndNode = null;
+ lastQuery = "";
+ }
+
+ /**
+ * Returns the number of elements.
+ */
+ public int size() { return nodes.size(); }
+
+ /**
+ * Adds a song group.
+ * @param beatmaps the list of beatmaps in the group
+ * @return the new BeatmapSetNode
+ */
+ public BeatmapSetNode addSongGroup(ArrayList beatmaps) {
+ BeatmapSet beatmapSet = new BeatmapSet(beatmaps);
+ BeatmapSetNode node = new BeatmapSetNode(beatmapSet);
+ parsedNodes.add(node);
+ mapCount += beatmaps.size();
+
+ // add beatmap set ID to set
+ int msid = beatmaps.get(0).beatmapSetID;
+ if (msid > 0)
+ MSIDdb.add(msid);
+
+ return node;
+ }
+
+ /**
+ * Deletes a song group from the list, and also deletes the beatmap
+ * directory associated with the node.
+ * @param node the node containing the song group to delete
+ * @return true if the song group was deleted, false otherwise
+ */
+ public boolean deleteSongGroup(BeatmapSetNode node) {
+ if (node == null)
+ return false;
+
+ // re-link base nodes
+ int index = node.index;
+ BeatmapSetNode ePrev = getBaseNode(index - 1), eCur = getBaseNode(index), eNext = getBaseNode(index + 1);
+ if (ePrev != null) {
+ if (ePrev.index == expandedIndex)
+ expandedEndNode.next = eNext;
+ else if (eNext != null && eNext.index == expandedIndex)
+ ePrev.next = expandedStartNode;
+ else
+ ePrev.next = eNext;
+ }
+ if (eNext != null) {
+ if (eNext.index == expandedIndex)
+ expandedStartNode.prev = ePrev;
+ else if (ePrev != null && ePrev.index == expandedIndex)
+ eNext.prev = expandedEndNode;
+ else
+ eNext.prev = ePrev;
+ }
+
+ // remove all node references
+ Beatmap beatmap = node.getBeatmapSet().get(0);
+ nodes.remove(index);
+ parsedNodes.remove(eCur);
+ mapCount -= node.getBeatmapSet().size();
+ if (beatmap.beatmapSetID > 0)
+ MSIDdb.remove(beatmap.beatmapSetID);
+
+ // reset indices
+ for (int i = index, size = size(); i < size; i++)
+ nodes.get(i).index = i;
+ if (index == expandedIndex) {
+ expandedIndex = -1;
+ expandedStartNode = expandedEndNode = null;
+ } else if (expandedIndex > index) {
+ expandedIndex--;
+ BeatmapSetNode expandedNode = expandedStartNode;
+ for (int i = 0, size = expandedNode.getBeatmapSet().size();
+ i < size && expandedNode != null;
+ i++, expandedNode = expandedNode.next)
+ expandedNode.index = expandedIndex;
+ }
+
+ // stop playing the track
+ File dir = beatmap.getFile().getParentFile();
+ if (MusicController.trackExists() || MusicController.isTrackLoading()) {
+ File audioFile = MusicController.getBeatmap().audioFilename;
+ if (audioFile != null && audioFile.equals(beatmap.audioFilename)) {
+ MusicController.reset();
+ System.gc(); // TODO: why can't files be deleted without calling this?
+ }
+ }
+
+ // remove entry from cache
+ BeatmapDB.delete(dir.getName());
+
+ // delete the associated directory
+ try {
+ Utils.deleteToTrash(dir);
+ } catch (IOException e) {
+ ErrorHandler.error("Could not delete song group.", e, true);
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes a song from a song group, and also deletes the beatmap file.
+ * If this causes the song group to be empty, then the song group and
+ * beatmap directory will be deleted altogether.
+ * @param node the node containing the song group to delete (expanded only)
+ * @return true if the song or song group was deleted, false otherwise
+ * @see #deleteSongGroup(BeatmapSetNode)
+ */
+ public boolean deleteSong(BeatmapSetNode node) {
+ if (node == null || node.beatmapIndex == -1 || node.index != expandedIndex)
+ return false;
+
+ // last song in group?
+ int size = node.getBeatmapSet().size();
+ if (size == 1)
+ return deleteSongGroup(node);
+
+ // reset indices
+ BeatmapSetNode expandedNode = node.next;
+ for (int i = node.beatmapIndex + 1;
+ i < size && expandedNode != null && expandedNode.index == node.index;
+ i++, expandedNode = expandedNode.next)
+ expandedNode.beatmapIndex--;
+
+ // remove song reference
+ Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
+ mapCount--;
+
+ // re-link nodes
+ if (node.prev != null)
+ node.prev.next = node.next;
+ if (node.next != null)
+ node.next.prev = node.prev;
+
+ // remove entry from cache
+ File file = beatmap.getFile();
+ BeatmapDB.delete(file.getParentFile().getName(), file.getName());
+
+ // delete the associated file
+ try {
+ Utils.deleteToTrash(file);
+ } catch (IOException e) {
+ ErrorHandler.error("Could not delete song.", e, true);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the total number of parsed maps (i.e. Beatmap objects).
+ */
+ public int getMapCount() { return mapCount; }
+
+ /**
+ * Returns the total number of parsed maps sets.
+ */
+ public int getMapSetCount() { return parsedNodes.size(); }
+
+ /**
+ * Returns the BeatmapSetNode at an index, disregarding expansions.
+ * @param index the node index
+ */
+ public BeatmapSetNode getBaseNode(int index) {
+ if (index < 0 || index >= size())
+ return null;
+
+ return nodes.get(index);
+ }
+
+ /**
+ * Returns a random base node.
+ */
+ public BeatmapSetNode getRandomNode() {
+ BeatmapSetNode node = getBaseNode((int) (Math.random() * size()));
+ if (node != null && node.index == expandedIndex) // don't choose an expanded group node
+ node = node.next;
+ return node;
+ }
+
+ /**
+ * Returns the BeatmapSetNode a given number of positions forward or backwards.
+ * @param node the starting node
+ * @param shift the number of nodes to shift forward (+) or backward (-).
+ */
+ public BeatmapSetNode getNode(BeatmapSetNode node, int shift) {
+ BeatmapSetNode startNode = node;
+ if (shift > 0) {
+ for (int i = 0; i < shift && startNode != null; i++)
+ startNode = startNode.next;
+ } else {
+ for (int i = 0; i < shift && startNode != null; i++)
+ startNode = startNode.prev;
+ }
+ return startNode;
+ }
+
+ /**
+ * Returns the index of the expanded node (or -1 if nothing is expanded).
+ */
+ public int getExpandedIndex() { return expandedIndex; }
+
+ /**
+ * Expands the node at an index by inserting a new node for each Beatmap
+ * in that node and hiding the group node.
+ * @return the first of the newly-inserted nodes
+ */
+ public BeatmapSetNode expand(int index) {
+ // undo the previous expansion
+ unexpand();
+
+ BeatmapSetNode node = getBaseNode(index);
+ if (node == null)
+ return null;
+
+ expandedStartNode = expandedEndNode = null;
+
+ // create new nodes
+ BeatmapSet beatmapSet = node.getBeatmapSet();
+ BeatmapSetNode prevNode = node.prev;
+ BeatmapSetNode nextNode = node.next;
+ for (int i = 0, size = beatmapSet.size(); i < size; i++) {
+ BeatmapSetNode newNode = new BeatmapSetNode(beatmapSet);
+ newNode.index = index;
+ newNode.beatmapIndex = i;
+ newNode.prev = node;
+
+ // unlink the group node
+ if (i == 0) {
+ expandedStartNode = newNode;
+ newNode.prev = prevNode;
+ if (prevNode != null)
+ prevNode.next = newNode;
+ }
+
+ node.next = newNode;
+ node = node.next;
+ }
+ if (nextNode != null) {
+ node.next = nextNode;
+ nextNode.prev = node;
+ }
+ expandedEndNode = node;
+
+ expandedIndex = index;
+ return expandedStartNode;
+ }
+
+ /**
+ * Undoes the current expansion, if any.
+ */
+ private void unexpand() {
+ if (expandedIndex < 0 || expandedIndex >= size())
+ return;
+
+ // recreate surrounding links
+ BeatmapSetNode
+ ePrev = getBaseNode(expandedIndex - 1),
+ eCur = getBaseNode(expandedIndex),
+ eNext = getBaseNode(expandedIndex + 1);
+ if (ePrev != null)
+ ePrev.next = eCur;
+ eCur.prev = ePrev;
+ eCur.index = expandedIndex;
+ eCur.next = eNext;
+ if (eNext != null)
+ eNext.prev = eCur;
+
+ expandedIndex = -1;
+ expandedStartNode = expandedEndNode = null;
+ return;
+ }
+
+ /**
+ * Initializes the links in the list.
+ */
+ public void init() {
+ if (size() < 1)
+ return;
+
+ // sort the list
+ Collections.sort(nodes, BeatmapSortOrder.getSort().getComparator());
+ expandedIndex = -1;
+ expandedStartNode = expandedEndNode = null;
+
+ // create links
+ BeatmapSetNode lastNode = nodes.get(0);
+ lastNode.index = 0;
+ lastNode.prev = null;
+ for (int i = 1, size = size(); i < size; i++) {
+ BeatmapSetNode node = nodes.get(i);
+ lastNode.next = node;
+ node.index = i;
+ node.prev = lastNode;
+
+ lastNode = node;
+ }
+ lastNode.next = null;
+ }
+
+ /**
+ * Creates a new list of song groups in which each group contains a match to a search query.
+ * @param query the search query (terms separated by spaces)
+ * @return false if query is the same as the previous one, true otherwise
+ */
+ public boolean search(String query) {
+ if (query == null)
+ return false;
+
+ // don't redo the same search
+ query = query.trim().toLowerCase();
+ if (lastQuery != null && query.equals(lastQuery))
+ return false;
+ lastQuery = query;
+ LinkedList terms = new LinkedList(Arrays.asList(query.split("\\s+")));
+
+ // if empty query, reset to original list
+ if (query.isEmpty() || terms.isEmpty()) {
+ nodes = parsedNodes;
+ return true;
+ }
+
+ // find and remove any conditional search terms
+ LinkedList condType = new LinkedList();
+ LinkedList condOperator = new LinkedList();
+ LinkedList condValue = new LinkedList();
+
+ Iterator termIter = terms.iterator();
+ while (termIter.hasNext()) {
+ String term = termIter.next();
+ Matcher m = SEARCH_CONDITION_PATTERN.matcher(term);
+ if (m.find()) {
+ condType.add(m.group(1));
+ condOperator.add(m.group(2));
+ condValue.add(Float.parseFloat(m.group(3)));
+ termIter.remove();
+ }
+ }
+
+ // build an initial list from first search term
+ nodes = new ArrayList();
+ if (terms.isEmpty()) {
+ // conditional term
+ String type = condType.remove();
+ String operator = condOperator.remove();
+ float value = condValue.remove();
+ for (BeatmapSetNode node : parsedNodes) {
+ if (node.getBeatmapSet().matches(type, operator, value))
+ nodes.add(node);
+ }
+ } else {
+ // normal term
+ String term = terms.remove();
+ for (BeatmapSetNode node : parsedNodes) {
+ if (node.getBeatmapSet().matches(term))
+ nodes.add(node);
+ }
+ }
+
+ // iterate through remaining normal search terms
+ while (!terms.isEmpty()) {
+ if (nodes.isEmpty())
+ return true;
+
+ String term = terms.remove();
+
+ // remove nodes from list if they don't match all terms
+ Iterator nodeIter = nodes.iterator();
+ while (nodeIter.hasNext()) {
+ BeatmapSetNode node = nodeIter.next();
+ if (!node.getBeatmapSet().matches(term))
+ nodeIter.remove();
+ }
+ }
+
+ // iterate through remaining conditional terms
+ while (!condType.isEmpty()) {
+ if (nodes.isEmpty())
+ return true;
+
+ String type = condType.remove();
+ String operator = condOperator.remove();
+ float value = condValue.remove();
+
+ // remove nodes from list if they don't match all terms
+ Iterator nodeIter = nodes.iterator();
+ while (nodeIter.hasNext()) {
+ BeatmapSetNode node = nodeIter.next();
+ if (!node.getBeatmapSet().matches(type, operator, value))
+ nodeIter.remove();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether or not the list contains the given beatmap set ID.
+ *
+ * Note that IDs for older maps might have been improperly parsed, so
+ * there is no guarantee that this method will return an accurate value.
+ * @param id the beatmap set ID to check
+ * @return true if id is in the list
+ */
+ public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
+}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
new file mode 100644
index 00000000..6324d1c4
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
@@ -0,0 +1,131 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.GameData.Grade;
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Options;
+import itdelatrisu.opsu.Utils;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Image;
+
+/**
+ * Node in an BeatmapSetList representing a beatmap set.
+ */
+public class BeatmapSetNode {
+ /** The associated beatmap set. */
+ private BeatmapSet beatmapSet;
+
+ /** Index of the selected beatmap (-1 if not focused). */
+ public int beatmapIndex = -1;
+
+ /** Index of this node. */
+ public int index = 0;
+
+ /** Links to other nodes. */
+ public BeatmapSetNode prev, next;
+
+ /**
+ * Constructor.
+ * @param beatmapSet the beatmap set
+ */
+ public BeatmapSetNode(BeatmapSet beatmapSet) {
+ this.beatmapSet = beatmapSet;
+ }
+
+ /**
+ * Returns the associated beatmap set.
+ * @return the beatmap set
+ */
+ public BeatmapSet getBeatmapSet() { return beatmapSet; }
+
+ /**
+ * Draws the button.
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param grade the highest grade, if any
+ * @param focus true if this is the focused node
+ */
+ public void draw(float x, float y, Grade grade, boolean focus) {
+ Image bg = GameImage.MENU_BUTTON_BG.getImage();
+ boolean expanded = (beatmapIndex > -1);
+ Beatmap beatmap;
+ bg.setAlpha(0.9f);
+ Color bgColor;
+ Color textColor = Options.getSkin().getSongSelectInactiveTextColor();
+
+ // get drawing parameters
+ if (expanded) {
+ x -= bg.getWidth() / 10f;
+ if (focus) {
+ bgColor = Color.white;
+ textColor = Options.getSkin().getSongSelectActiveTextColor();
+ } else
+ bgColor = Utils.COLOR_BLUE_BUTTON;
+ beatmap = beatmapSet.get(beatmapIndex);
+ } else {
+ bgColor = Utils.COLOR_ORANGE_BUTTON;
+ beatmap = beatmapSet.get(0);
+ }
+ bg.draw(x, y, bgColor);
+
+ float cx = x + (bg.getWidth() * 0.043f);
+ float cy = y + (bg.getHeight() * 0.2f) - 3;
+
+ // draw grade
+ if (grade != Grade.NULL) {
+ Image gradeImg = grade.getMenuImage();
+ gradeImg.drawCentered(cx - bg.getWidth() * 0.01f + gradeImg.getWidth() / 2f, y + bg.getHeight() / 2.2f);
+ cx += gradeImg.getWidth();
+ }
+
+ // draw text
+ if (Options.useUnicodeMetadata()) { // load glyphs
+ Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
+ Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
+ }
+ Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
+ Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
+ String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
+ if (expanded || beatmapSet.size() == 1)
+ Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 4,
+ beatmap.version, textColor);
+ }
+
+ /**
+ * Returns an array of strings containing beatmap information for the
+ * selected beatmap, or null if none selected.
+ * @see BeatmapSet#getInfo(int)
+ */
+ public String[] getInfo() { return (beatmapIndex < 0) ? null : beatmapSet.getInfo(beatmapIndex); }
+
+ /**
+ * Returns a formatted string for the beatmap at {@code beatmapIndex}:
+ * "Artist - Title [Version]" (version omitted if {@code beatmapIndex} is invalid)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ if (beatmapIndex == -1)
+ return beatmapSet.toString();
+ else
+ return beatmapSet.get(beatmapIndex).toString();
+ }
+}
\ No newline at end of file
diff --git a/src/itdelatrisu/opsu/SongSort.java b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
similarity index 59%
rename from src/itdelatrisu/opsu/SongSort.java
rename to src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
index db0c69a2..a9c3c545 100644
--- a/src/itdelatrisu/opsu/SongSort.java
+++ b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java
@@ -16,7 +16,11 @@
* along with opsu!. If not, see .
*/
-package itdelatrisu.opsu;
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.util.Arrays;
import java.util.Collections;
@@ -25,9 +29,9 @@ import java.util.Comparator;
import org.newdawn.slick.Image;
/**
- * OsuGroupNode sorts.
+ * Beatmap sorting orders.
*/
-public enum SongSort {
+public enum BeatmapSortOrder {
TITLE (0, "Title", new TitleOrder()),
ARTIST (1, "Artist", new ArtistOrder()),
CREATOR (2, "Creator", new CreatorOrder()),
@@ -41,7 +45,7 @@ public enum SongSort {
private String name;
/** The comparator for the sort. */
- private Comparator comparator;
+ private Comparator comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab;
@@ -49,83 +53,85 @@ public enum SongSort {
/** Total number of sorts. */
private static final int SIZE = values().length;
- /** Array of SongSort objects in reverse order. */
- public static final SongSort[] VALUES_REVERSED;
+ /** Array of BeatmapSortOrder objects in reverse order. */
+ public static final BeatmapSortOrder[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current sort. */
- private static SongSort currentSort = TITLE;
+ private static BeatmapSortOrder currentSort = TITLE;
/**
* Returns the current sort.
* @return the current sort
*/
- public static SongSort getSort() { return currentSort; }
+ public static BeatmapSortOrder getSort() { return currentSort; }
/**
* Sets a new sort.
* @param sort the new sort
*/
- public static void setSort(SongSort sort) { SongSort.currentSort = sort; }
+ public static void setSort(BeatmapSortOrder sort) { BeatmapSortOrder.currentSort = sort; }
/**
- * Compares two OsuGroupNode objects by title.
+ * Compares two BeatmapSetNode objects by title.
*/
- private static class TitleOrder implements Comparator {
+ private static class TitleOrder implements Comparator {
@Override
- public int compare(OsuGroupNode v, OsuGroupNode w) {
- return v.osuFiles.get(0).title.compareToIgnoreCase(w.osuFiles.get(0).title);
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ return v.getBeatmapSet().get(0).title.compareToIgnoreCase(w.getBeatmapSet().get(0).title);
}
}
/**
- * Compares two OsuGroupNode objects by artist.
+ * Compares two BeatmapSetNode objects by artist.
*/
- private static class ArtistOrder implements Comparator {
+ private static class ArtistOrder implements Comparator {
@Override
- public int compare(OsuGroupNode v, OsuGroupNode w) {
- return v.osuFiles.get(0).artist.compareToIgnoreCase(w.osuFiles.get(0).artist);
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ return v.getBeatmapSet().get(0).artist.compareToIgnoreCase(w.getBeatmapSet().get(0).artist);
}
}
/**
- * Compares two OsuGroupNode objects by creator.
+ * Compares two BeatmapSetNode objects by creator.
*/
- private static class CreatorOrder implements Comparator {
+ private static class CreatorOrder implements Comparator {
@Override
- public int compare(OsuGroupNode v, OsuGroupNode w) {
- return v.osuFiles.get(0).creator.compareToIgnoreCase(w.osuFiles.get(0).creator);
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ return v.getBeatmapSet().get(0).creator.compareToIgnoreCase(w.getBeatmapSet().get(0).creator);
}
}
/**
- * Compares two OsuGroupNode objects by BPM.
+ * Compares two BeatmapSetNode objects by BPM.
*/
- private static class BPMOrder implements Comparator {
+ private static class BPMOrder implements Comparator {
@Override
- public int compare(OsuGroupNode v, OsuGroupNode w) {
- return Integer.compare(v.osuFiles.get(0).bpmMax, w.osuFiles.get(0).bpmMax);
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
+ return Integer.compare(v.getBeatmapSet().get(0).bpmMax, w.getBeatmapSet().get(0).bpmMax);
}
}
/**
- * Compares two OsuGroupNode objects by length.
+ * Compares two BeatmapSetNode objects by length.
* Uses the longest beatmap in each set for comparison.
*/
- private static class LengthOrder implements Comparator {
+ private static class LengthOrder implements Comparator {
@Override
- public int compare(OsuGroupNode v, OsuGroupNode w) {
+ public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vMax = 0, wMax = 0;
- for (OsuFile osu : v.osuFiles) {
- if (osu.endTime > vMax)
- vMax = osu.endTime;
+ for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
+ Beatmap beatmap = v.getBeatmapSet().get(i);
+ if (beatmap.endTime > vMax)
+ vMax = beatmap.endTime;
}
- for (OsuFile osu : w.osuFiles) {
- if (osu.endTime > wMax)
- wMax = osu.endTime;
+ for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
+ Beatmap beatmap = w.getBeatmapSet().get(i);
+ if (beatmap.endTime > wMax)
+ wMax = beatmap.endTime;
}
return Integer.compare(vMax, wMax);
}
@@ -137,7 +143,7 @@ public enum SongSort {
* @param name the sort name
* @param comparator the comparator for the sort
*/
- SongSort(int id, String name, Comparator comparator) {
+ BeatmapSortOrder(int id, String name, Comparator comparator) {
this.id = id;
this.name = name;
this.comparator = comparator;
@@ -167,7 +173,7 @@ public enum SongSort {
* Returns the comparator for the sort.
* @return the comparator
*/
- public Comparator getComparator() { return comparator; }
+ public Comparator getComparator() { return comparator; }
/**
* Checks if the coordinates are within the image bounds.
diff --git a/src/itdelatrisu/opsu/beatmap/HitObject.java b/src/itdelatrisu/opsu/beatmap/HitObject.java
new file mode 100644
index 00000000..f32997ce
--- /dev/null
+++ b/src/itdelatrisu/opsu/beatmap/HitObject.java
@@ -0,0 +1,540 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.GameMod;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+/**
+ * Data type representing a parsed hit object.
+ */
+public class HitObject {
+ /** Hit object types (bits). */
+ public static final int
+ TYPE_CIRCLE = 1,
+ TYPE_SLIDER = 2,
+ TYPE_NEWCOMBO = 4, // not an object
+ TYPE_SPINNER = 8;
+
+ /** Hit object type names. */
+ private static final String
+ CIRCLE = "circle",
+ SLIDER = "slider",
+ SPINNER = "spinner",
+ UNKNOWN = "unknown object";
+
+ /** Hit sound types (bits). */
+ public static final byte
+ SOUND_NORMAL = 0,
+ SOUND_WHISTLE = 2,
+ SOUND_FINISH = 4,
+ SOUND_CLAP = 8;
+
+ /**
+ * Slider curve types.
+ * (Deprecated: only Beziers are currently used.)
+ */
+ public static final char
+ SLIDER_CATMULL = 'C',
+ SLIDER_BEZIER = 'B',
+ SLIDER_LINEAR = 'L',
+ SLIDER_PASSTHROUGH = 'P';
+
+ /** Max hit object coordinates. */
+ private static final int
+ MAX_X = 512,
+ MAX_Y = 384;
+
+ /** The x and y multipliers for hit object coordinates. */
+ private static float xMultiplier, yMultiplier;
+
+ /** The x and y offsets for hit object coordinates. */
+ private static int
+ xOffset, // offset right of border
+ yOffset; // offset below health bar
+
+ /** The container height. */
+ private static int containerHeight;
+
+ /** The offset per stack. */
+ private static float stackOffset;
+
+ /**
+ * Returns the stack position modifier, in pixels.
+ * @return stack position modifier
+ */
+ public static float getStackOffset() { return stackOffset; }
+
+ /**
+ * Sets the stack position modifier.
+ * @param offset stack position modifier, in pixels
+ */
+ public static void setStackOffset(float offset) { stackOffset = offset; }
+
+ /** Starting coordinates. */
+ private float x, y;
+
+ /** Start time (in ms). */
+ private int time;
+
+ /** Hit object type (TYPE_* bitmask). */
+ private int type;
+
+ /** Hit sound type (SOUND_* bitmask). */
+ private byte hitSound;
+
+ /** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
+ private byte[] addition;
+
+ /** Slider curve type (SLIDER_* constant). */
+ private char sliderType;
+
+ /** Slider coordinate lists. */
+ private float[] sliderX, sliderY;
+
+ /** Slider repeat count. */
+ private int repeat;
+
+ /** Slider pixel length. */
+ private float pixelLength;
+
+ /** Spinner end time (in ms). */
+ private int endTime;
+
+ /** Slider edge hit sound type (SOUND_* bitmask). */
+ private byte[] edgeHitSound;
+
+ /** Slider edge hit sound addition (sampleSet, AdditionSampleSet). */
+ private byte[][] edgeAddition;
+
+ /** Current index in combo color array. */
+ private int comboIndex;
+
+ /** Number to display in hit object. */
+ private int comboNumber;
+
+ /** Hit object index in the current stack. */
+ private int stack;
+
+ /**
+ * Initializes the HitObject data type with container dimensions.
+ * @param width the container width
+ * @param height the container height
+ */
+ public static void init(int width, int height) {
+ containerHeight = height;
+ int swidth = width;
+ int sheight = height;
+ if (swidth * 3 > sheight * 4)
+ swidth = sheight * 4 / 3;
+ else
+ sheight = swidth * 3 / 4;
+ xMultiplier = swidth / 640f;
+ yMultiplier = sheight / 480f;
+ xOffset = (int) (width - MAX_X * xMultiplier) / 2;
+ yOffset = (int) (height - MAX_Y * yMultiplier) / 2;
+ }
+
+ /**
+ * Returns the X multiplier for coordinates.
+ */
+ public static float getXMultiplier() { return xMultiplier; }
+
+ /**
+ * Returns the Y multiplier for coordinates.
+ */
+ public static float getYMultiplier() { return yMultiplier; }
+
+ /**
+ * Returns the X offset for coordinates.
+ */
+ public static int getXOffset() { return xOffset; }
+
+ /**
+ * Returns the Y offset for coordinates.
+ */
+ public static int getYOffset() { return yOffset; }
+
+ /**
+ * Constructor.
+ * @param line the line to be parsed
+ */
+ public HitObject(String line) {
+ /**
+ * [OBJECT FORMATS]
+ * Circles:
+ * x,y,time,type,hitSound,addition
+ * 256,148,9466,1,2,0:0:0:0:
+ *
+ * Sliders:
+ * x,y,time,type,hitSound,sliderType|curveX:curveY|...,repeat,pixelLength,edgeHitsound,edgeAddition,addition
+ * 300,68,4591,2,0,B|372:100|332:172|420:192,2,180,2|2|2,0:0|0:0|0:0,0:0:0:0:
+ *
+ * Spinners:
+ * x,y,time,type,hitSound,endTime,addition
+ * 256,192,654,12,0,4029,0:0:0:0:
+ *
+ * NOTE: 'addition' -> sampl:add:cust:vol:hitsound (optional, defaults to "0:0:0:0:")
+ */
+ String tokens[] = line.split(",");
+
+ // common fields
+ this.x = Float.parseFloat(tokens[0]);
+ this.y = Float.parseFloat(tokens[1]);
+ this.time = Integer.parseInt(tokens[2]);
+ this.type = Integer.parseInt(tokens[3]);
+ this.hitSound = Byte.parseByte(tokens[4]);
+
+ // type-specific fields
+ int additionIndex;
+ if ((type & HitObject.TYPE_CIRCLE) > 0)
+ additionIndex = 5;
+ else if ((type & HitObject.TYPE_SLIDER) > 0) {
+ additionIndex = 10;
+
+ // slider curve type and coordinates
+ String[] sliderTokens = tokens[5].split("\\|");
+ this.sliderType = sliderTokens[0].charAt(0);
+ this.sliderX = new float[sliderTokens.length - 1];
+ this.sliderY = new float[sliderTokens.length - 1];
+ for (int j = 1; j < sliderTokens.length; j++) {
+ String[] sliderXY = sliderTokens[j].split(":");
+ this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]);
+ this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]);
+ }
+ this.repeat = Integer.parseInt(tokens[6]);
+ this.pixelLength = Float.parseFloat(tokens[7]);
+ if (tokens.length > 8) {
+ String[] edgeHitSoundTokens = tokens[8].split("\\|");
+ this.edgeHitSound = new byte[edgeHitSoundTokens.length];
+ for (int j = 0; j < edgeHitSoundTokens.length; j++)
+ edgeHitSound[j] = Byte.parseByte(edgeHitSoundTokens[j]);
+ }
+ if (tokens.length > 9) {
+ String[] edgeAdditionTokens = tokens[9].split("\\|");
+ this.edgeAddition = new byte[edgeAdditionTokens.length][2];
+ for (int j = 0; j < edgeAdditionTokens.length; j++) {
+ String[] tedgeAddition = edgeAdditionTokens[j].split(":");
+ edgeAddition[j][0] = Byte.parseByte(tedgeAddition[0]);
+ edgeAddition[j][1] = Byte.parseByte(tedgeAddition[1]);
+ }
+ }
+ } else { //if ((type & HitObject.TYPE_SPINNER) > 0) {
+ additionIndex = 6;
+
+ // some 'endTime' fields contain a ':' character (?)
+ int index = tokens[5].indexOf(':');
+ if (index != -1)
+ tokens[5] = tokens[5].substring(0, index);
+ this.endTime = Integer.parseInt(tokens[5]);
+ }
+
+ // addition
+ if (tokens.length > additionIndex) {
+ String[] additionTokens = tokens[additionIndex].split(":");
+ this.addition = new byte[additionTokens.length];
+ for (int j = 0; j < additionTokens.length; j++)
+ this.addition[j] = Byte.parseByte(additionTokens[j]);
+ }
+ }
+
+ /**
+ * Returns the raw starting x coordinate.
+ */
+ public float getX() { return x; }
+
+ /**
+ * Returns the raw starting y coordinate.
+ */
+ public float getY() { return y; }
+
+ /**
+ * Returns the scaled starting x coordinate.
+ */
+ public float getScaledX() { return (x - stack * stackOffset) * xMultiplier + xOffset; }
+
+ /**
+ * Returns the scaled starting y coordinate.
+ */
+ public float getScaledY() {
+ if (GameMod.HARD_ROCK.isActive())
+ return containerHeight - ((y + stack * stackOffset) * yMultiplier + yOffset);
+ else
+ return (y - stack * stackOffset) * yMultiplier + yOffset;
+ }
+
+ /**
+ * Returns the start time.
+ * @return the start time (in ms)
+ */
+ public int getTime() { return time; }
+
+ /**
+ * Returns the hit object type.
+ * @return the object type (TYPE_* bitmask)
+ */
+ public int getType() { return type; }
+
+ /**
+ * Returns the name of the hit object type.
+ */
+ public String getTypeName() {
+ if (isCircle())
+ return CIRCLE;
+ else if (isSlider())
+ return SLIDER;
+ else if (isSpinner())
+ return SPINNER;
+ else
+ return UNKNOWN;
+ }
+
+ /**
+ * Returns the hit sound type.
+ * @return the sound type (SOUND_* bitmask)
+ */
+ public byte getHitSoundType() { return hitSound; }
+
+ /**
+ * Returns the edge hit sound type.
+ * @param index the slider edge index (ignored for non-sliders)
+ * @return the sound type (SOUND_* bitmask)
+ */
+ public byte getEdgeHitSoundType(int index) {
+ if (edgeHitSound != null)
+ return edgeHitSound[index];
+ else
+ return hitSound;
+ }
+
+ /**
+ * Returns the slider type.
+ * @return the slider type (SLIDER_* constant)
+ */
+ public char getSliderType() { return sliderType; }
+
+ /**
+ * Returns a list of raw slider x coordinates.
+ */
+ public float[] getSliderX() { return sliderX; }
+
+ /**
+ * Returns a list of raw slider y coordinates.
+ */
+ public float[] getSliderY() { return sliderY; }
+
+ /**
+ * Returns a list of scaled slider x coordinates.
+ * Note that this method will create a new array.
+ */
+ public float[] getScaledSliderX() {
+ if (sliderX == null)
+ return null;
+
+ float[] x = new float[sliderX.length];
+ for (int i = 0; i < x.length; i++)
+ x[i] = (sliderX[i] - stack * stackOffset) * xMultiplier + xOffset;
+ return x;
+ }
+
+ /**
+ * Returns a list of scaled slider y coordinates.
+ * Note that this method will create a new array.
+ */
+ public float[] getScaledSliderY() {
+ if (sliderY == null)
+ return null;
+
+ float[] y = new float[sliderY.length];
+ if (GameMod.HARD_ROCK.isActive()) {
+ for (int i = 0; i < y.length; i++)
+ y[i] = containerHeight - ((sliderY[i] + stack * stackOffset) * yMultiplier + yOffset);
+ } else {
+ for (int i = 0; i < y.length; i++)
+ y[i] = (sliderY[i] - stack * stackOffset) * yMultiplier + yOffset;
+ }
+ return y;
+ }
+
+ /**
+ * Returns the slider repeat count.
+ * @return the repeat count
+ */
+ public int getRepeatCount() { return repeat; }
+
+ /**
+ * Returns the slider pixel length.
+ * @return the pixel length
+ */
+ public float getPixelLength() { return pixelLength; }
+
+ /**
+ * Returns the spinner end time.
+ * @return the end time (in ms)
+ */
+ public int getEndTime() { return endTime; }
+
+ /**
+ * Sets the current index in the combo color array.
+ * @param comboIndex the combo index
+ */
+ public void setComboIndex(int comboIndex) { this.comboIndex = comboIndex; }
+
+ /**
+ * Returns the current index in the combo color array.
+ * @return the combo index
+ */
+ public int getComboIndex() { return comboIndex; }
+
+ /**
+ * Sets the number to display in the hit object.
+ * @param comboNumber the combo number
+ */
+ public void setComboNumber(int comboNumber) { this.comboNumber = comboNumber; }
+
+ /**
+ * Returns the number to display in the hit object.
+ * @return the combo number
+ */
+ public int getComboNumber() { return comboNumber; }
+
+ /**
+ * Returns whether or not the hit object is a circle.
+ * @return true if circle
+ */
+ public boolean isCircle() { return (type & TYPE_CIRCLE) > 0; }
+
+ /**
+ * Returns whether or not the hit object is a slider.
+ * @return true if slider
+ */
+ public boolean isSlider() { return (type & TYPE_SLIDER) > 0; }
+
+ /**
+ * Returns whether or not the hit object is a spinner.
+ * @return true if spinner
+ */
+ public boolean isSpinner() { return (type & TYPE_SPINNER) > 0; }
+
+ /**
+ * Returns whether or not the hit object starts a new combo.
+ * @return true if new combo
+ */
+ public boolean isNewCombo() { return (type & TYPE_NEWCOMBO) > 0; }
+
+ /**
+ * Returns the number of extra skips on the combo colors.
+ */
+ public int getComboSkip() { return (type >> TYPE_NEWCOMBO); }
+
+ /**
+ * Returns the sample set at the given index.
+ * @param index the index (for sliders, ignored otherwise)
+ * @return the sample set, or 0 if none available
+ */
+ public byte getSampleSet(int index) {
+ if (edgeAddition != null)
+ return edgeAddition[index][0];
+ if (addition != null)
+ return addition[0];
+ return 0;
+ }
+
+ /**
+ * Returns the 'addition' sample set at the given index.
+ * @param index the index (for sliders, ignored otherwise)
+ * @return the sample set, or 0 if none available
+ */
+ public byte getAdditionSampleSet(int index) {
+ if (edgeAddition != null)
+ return edgeAddition[index][1];
+ if (addition != null)
+ return addition[1];
+ return 0;
+ }
+
+ /**
+ * Sets the hit object index in the current stack.
+ * @param stack index in the stack
+ */
+ public void setStack(int stack) { this.stack = stack; }
+
+ /**
+ * Returns the hit object index in the current stack.
+ * @return index in the stack
+ */
+ public int getStack() { return stack; }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ NumberFormat nf = new DecimalFormat("###.#####");
+
+ // common fields
+ sb.append(nf.format(x)); sb.append(',');
+ sb.append(nf.format(y)); sb.append(',');
+ sb.append(time); sb.append(',');
+ sb.append(type); sb.append(',');
+ sb.append(hitSound); sb.append(',');
+
+ // type-specific fields
+ if (isCircle())
+ ;
+ else if (isSlider()) {
+ sb.append(getSliderType());
+ sb.append('|');
+ for (int i = 0; i < sliderX.length; i++) {
+ sb.append(nf.format(sliderX[i])); sb.append(':');
+ sb.append(nf.format(sliderY[i])); sb.append('|');
+ }
+ sb.setCharAt(sb.length() - 1, ',');
+ sb.append(repeat); sb.append(',');
+ sb.append(pixelLength); sb.append(',');
+ if (edgeHitSound != null) {
+ for (int i = 0; i < edgeHitSound.length; i++) {
+ sb.append(edgeHitSound[i]); sb.append('|');
+ }
+ sb.setCharAt(sb.length() - 1, ',');
+ }
+ if (edgeAddition != null) {
+ for (int i = 0; i < edgeAddition.length; i++) {
+ sb.append(edgeAddition[i][0]); sb.append(':');
+ sb.append(edgeAddition[i][1]); sb.append('|');
+ }
+ sb.setCharAt(sb.length() - 1, ',');
+ }
+ } else if (isSpinner()) {
+ sb.append(endTime);
+ sb.append(',');
+ }
+
+ // addition
+ if (addition != null) {
+ for (int i = 0; i < addition.length; i++) {
+ sb.append(addition[i]);
+ sb.append(':');
+ }
+ } else
+ sb.setLength(sb.length() - 1);
+
+ return sb.toString();
+ }
+}
diff --git a/src/itdelatrisu/opsu/OsuTimingPoint.java b/src/itdelatrisu/opsu/beatmap/TimingPoint.java
similarity index 94%
rename from src/itdelatrisu/opsu/OsuTimingPoint.java
rename to src/itdelatrisu/opsu/beatmap/TimingPoint.java
index be99b486..d508d270 100644
--- a/src/itdelatrisu/opsu/OsuTimingPoint.java
+++ b/src/itdelatrisu/opsu/beatmap/TimingPoint.java
@@ -16,14 +16,16 @@
* along with opsu!. If not, see .
*/
-package itdelatrisu.opsu;
+package itdelatrisu.opsu.beatmap;
+
+import itdelatrisu.opsu.Utils;
import org.newdawn.slick.util.Log;
/**
* Data type representing a timing point.
*/
-public class OsuTimingPoint {
+public class TimingPoint {
/** Timing point start time/offset (in ms). */
private int time = 0;
@@ -55,7 +57,7 @@ public class OsuTimingPoint {
* Constructor.
* @param line the line to be parsed
*/
- public OsuTimingPoint(String line) {
+ public TimingPoint(String line) {
// TODO: better support for old formats
String[] tokens = line.split(",");
try {
@@ -64,9 +66,9 @@ public class OsuTimingPoint {
this.sampleType = Byte.parseByte(tokens[3]);
this.sampleTypeCustom = Byte.parseByte(tokens[4]);
this.sampleVolume = Integer.parseInt(tokens[5]);
-// this.inherited = (Integer.parseInt(tokens[6]) == 1);
+// this.inherited = Utils.parseBoolean(tokens[6]);
if (tokens.length > 7)
- this.kiai = (Integer.parseInt(tokens[7]) == 1);
+ this.kiai = Utils.parseBoolean(tokens[7]);
} catch (ArrayIndexOutOfBoundsException e) {
Log.debug(String.format("Error parsing timing point: '%s'", line));
}
diff --git a/src/itdelatrisu/opsu/db/BeatmapDB.java b/src/itdelatrisu/opsu/db/BeatmapDB.java
new file mode 100644
index 00000000..3f85373e
--- /dev/null
+++ b/src/itdelatrisu/opsu/db/BeatmapDB.java
@@ -0,0 +1,590 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.db;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Options;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.newdawn.slick.util.Log;
+
+/**
+ * Handles connections and queries with the cached beatmap database.
+ */
+public class BeatmapDB {
+ /**
+ * Current database version.
+ * This value should be changed whenever the database format changes.
+ */
+ private static final String DATABASE_VERSION = "2014-06-08";
+
+ /** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
+ private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
+
+ /** Minimum batch size to invoke batch insertion. */
+ private static final int INSERT_BATCH_MIN = 100;
+
+ /** Beatmap loading flags. */
+ public static final int LOAD_NONARRAY = 1, LOAD_ARRAY = 2, LOAD_ALL = 3;
+
+ /** Database connection. */
+ private static Connection connection;
+
+ /** Query statements. */
+ private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, updateSizeStmt;
+
+ /** Current size of beatmap cache table. */
+ private static int cacheSize = -1;
+
+ // This class should not be instantiated.
+ private BeatmapDB() {}
+
+ /**
+ * Initializes the database connection.
+ */
+ public static void init() {
+ // create a database connection
+ connection = DBController.createConnection(Options.BEATMAP_DB.getPath());
+ if (connection == null)
+ return;
+
+ // create the database
+ createDatabase();
+
+ // prepare sql statements (used below)
+ try {
+ updateSizeStmt = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('size', ?)");
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
+ }
+
+ // retrieve the cache size
+ getCacheSize();
+
+ // check the database version
+ checkVersion();
+
+ // prepare sql statements (not used here)
+ try {
+ insertStmt = connection.prepareStatement(
+ "INSERT INTO beatmaps VALUES (" +
+ "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
+ "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ );
+ selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
+ deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
+ deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
+ }
+ }
+
+ /**
+ * Creates the database, if it does not exist.
+ */
+ private static void createDatabase() {
+ try (Statement stmt = connection.createStatement()) {
+ String sql =
+ "CREATE TABLE IF NOT EXISTS beatmaps (" +
+ "dir TEXT, file TEXT, lastModified INTEGER, " +
+ "MID INTEGER, MSID INTEGER, " +
+ "title TEXT, titleUnicode TEXT, artist TEXT, artistUnicode TEXT, " +
+ "creator TEXT, version TEXT, source TEXT, tags TEXT, " +
+ "circles INTEGER, sliders INTEGER, spinners INTEGER, " +
+ "hp REAL, cs REAL, od REAL, ar REAL, sliderMultiplier REAL, sliderTickRate REAL, " +
+ "bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
+ "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
+ "mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
+ "bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT" +
+ "); " +
+ "CREATE TABLE IF NOT EXISTS info (" +
+ "key TEXT NOT NULL UNIQUE, value TEXT" +
+ "); " +
+ "CREATE INDEX IF NOT EXISTS idx ON beatmaps (dir, file); " +
+
+ // extra optimizations
+ "PRAGMA locking_mode = EXCLUSIVE; " +
+ "PRAGMA journal_mode = WAL;";
+ stmt.executeUpdate(sql);
+
+ // set the version key, if empty
+ sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', '%s')", DATABASE_VERSION);
+ stmt.executeUpdate(sql);
+ } catch (SQLException e) {
+ ErrorHandler.error("Could not create beatmap database.", e, true);
+ }
+ }
+
+ /**
+ * Checks the stored table version, clears the beatmap database if different
+ * from the current version, then updates the version field.
+ */
+ private static void checkVersion() {
+ try (Statement stmt = connection.createStatement()) {
+ // get the stored version
+ String sql = "SELECT value FROM info WHERE key = 'version'";
+ ResultSet rs = stmt.executeQuery(sql);
+ String version = (rs.next()) ? rs.getString(1) : "";
+ rs.close();
+
+ // if different from current version, clear the database
+ if (!version.equals(DATABASE_VERSION)) {
+ clearDatabase();
+
+ // update version
+ PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
+ ps.setString(1, DATABASE_VERSION);
+ ps.executeUpdate();
+ ps.close();
+ }
+ } catch (SQLException e) {
+ ErrorHandler.error("Beatmap database version checks failed.", e, true);
+ }
+ }
+
+ /**
+ * Retrieves the size of the beatmap cache from the 'info' table.
+ */
+ private static void getCacheSize() {
+ try (Statement stmt = connection.createStatement()) {
+ String sql = "SELECT value FROM info WHERE key = 'size'";
+ ResultSet rs = stmt.executeQuery(sql);
+ try {
+ cacheSize = (rs.next()) ? Integer.parseInt(rs.getString(1)) : 0;
+ } catch (NumberFormatException e) {
+ cacheSize = 0;
+ }
+ rs.close();
+ } catch (SQLException e) {
+ ErrorHandler.error("Could not get beatmap cache size.", e, true);
+ }
+ }
+
+ /**
+ * Updates the size of the beatmap cache in the 'info' table.
+ */
+ private static void updateCacheSize() {
+ if (connection == null)
+ return;
+
+ try {
+ updateSizeStmt.setString(1, Integer.toString(Math.max(cacheSize, 0)));
+ updateSizeStmt.executeUpdate();
+ } catch (SQLException e) {
+ ErrorHandler.error("Could not update beatmap cache size.", e, true);
+ }
+ }
+
+ /**
+ * Clears the database.
+ */
+ public static void clearDatabase() {
+ if (connection == null)
+ return;
+
+ // drop the table, then recreate it
+ try (Statement stmt = connection.createStatement()) {
+ String sql = "DROP TABLE beatmaps";
+ stmt.executeUpdate(sql);
+ cacheSize = 0;
+ updateCacheSize();
+ } catch (SQLException e) {
+ ErrorHandler.error("Could not drop beatmap database.", e, true);
+ }
+ createDatabase();
+ }
+
+ /**
+ * Adds the beatmap to the database.
+ * @param beatmap the beatmap
+ */
+ public static void insert(Beatmap beatmap) {
+ if (connection == null)
+ return;
+
+ try {
+ setStatementFields(insertStmt, beatmap);
+ cacheSize += insertStmt.executeUpdate();
+ updateCacheSize();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to add beatmap to database.", e, true);
+ }
+ }
+
+ /**
+ * Adds the beatmaps to the database in a batch.
+ * @param batch a list of beatmaps
+ */
+ public static void insert(List batch) {
+ if (connection == null)
+ return;
+
+ try (Statement stmt = connection.createStatement()) {
+ // turn off auto-commit mode
+ boolean autoCommit = connection.getAutoCommit();
+ connection.setAutoCommit(false);
+
+ // drop indexes
+ boolean recreateIndexes = (batch.size() >= INSERT_BATCH_MIN);
+ if (recreateIndexes) {
+ String sql = "DROP INDEX IF EXISTS idx";
+ stmt.executeUpdate(sql);
+ }
+
+ // batch insert
+ for (Beatmap beatmap : batch) {
+ try {
+ setStatementFields(insertStmt, beatmap);
+ } catch (SQLException e) {
+ Log.error(String.format("Failed to insert map '%s' into database.", beatmap.getFile().getPath()), e);
+ continue;
+ }
+ insertStmt.addBatch();
+ }
+ int[] results = insertStmt.executeBatch();
+ for (int i = 0; i < results.length; i++) {
+ if (results[i] > 0)
+ cacheSize += results[i];
+ }
+
+ // re-create indexes
+ if (recreateIndexes) {
+ String sql = "CREATE INDEX idx ON beatmaps (dir, file)";
+ stmt.executeUpdate(sql);
+ }
+
+ // restore previous auto-commit mode
+ connection.commit();
+ connection.setAutoCommit(autoCommit);
+
+ // update cache size
+ updateCacheSize();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to add beatmaps to database.", e, true);
+ }
+ }
+
+ /**
+ * Sets all statement fields using a given beatmap.
+ * @param stmt the statement to set fields for
+ * @param beatmap the beatmap
+ * @throws SQLException
+ */
+ private static void setStatementFields(PreparedStatement stmt, Beatmap beatmap)
+ throws SQLException {
+ try {
+ stmt.setString(1, beatmap.getFile().getParentFile().getName());
+ stmt.setString(2, beatmap.getFile().getName());
+ stmt.setLong(3, beatmap.getFile().lastModified());
+ stmt.setInt(4, beatmap.beatmapID);
+ stmt.setInt(5, beatmap.beatmapSetID);
+ stmt.setString(6, beatmap.title);
+ stmt.setString(7, beatmap.titleUnicode);
+ stmt.setString(8, beatmap.artist);
+ stmt.setString(9, beatmap.artistUnicode);
+ stmt.setString(10, beatmap.creator);
+ stmt.setString(11, beatmap.version);
+ stmt.setString(12, beatmap.source);
+ stmt.setString(13, beatmap.tags);
+ stmt.setInt(14, beatmap.hitObjectCircle);
+ stmt.setInt(15, beatmap.hitObjectSlider);
+ stmt.setInt(16, beatmap.hitObjectSpinner);
+ stmt.setFloat(17, beatmap.HPDrainRate);
+ stmt.setFloat(18, beatmap.circleSize);
+ stmt.setFloat(19, beatmap.overallDifficulty);
+ stmt.setFloat(20, beatmap.approachRate);
+ stmt.setFloat(21, beatmap.sliderMultiplier);
+ stmt.setFloat(22, beatmap.sliderTickRate);
+ stmt.setInt(23, beatmap.bpmMin);
+ stmt.setInt(24, beatmap.bpmMax);
+ stmt.setInt(25, beatmap.endTime);
+ stmt.setString(26, beatmap.audioFilename.getName());
+ stmt.setInt(27, beatmap.audioLeadIn);
+ stmt.setInt(28, beatmap.previewTime);
+ stmt.setByte(29, beatmap.countdown);
+ stmt.setString(30, beatmap.sampleSet);
+ stmt.setFloat(31, beatmap.stackLeniency);
+ stmt.setByte(32, beatmap.mode);
+ stmt.setBoolean(33, beatmap.letterboxInBreaks);
+ stmt.setBoolean(34, beatmap.widescreenStoryboard);
+ stmt.setBoolean(35, beatmap.epilepsyWarning);
+ stmt.setString(36, (beatmap.bg == null) ? null : beatmap.bg.getName());
+ stmt.setString(37, beatmap.sliderBorderToString());
+ stmt.setString(38, beatmap.timingPointsToString());
+ stmt.setString(39, beatmap.breaksToString());
+ stmt.setString(40, beatmap.comboToString());
+ } catch (SQLException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SQLException(e);
+ }
+ }
+
+ /**
+ * Loads beatmap fields from the database.
+ * @param beatmap the beatmap
+ * @param flag whether to load all fields (LOAD_ALL), non-array
+ * fields (LOAD_NONARRAY), or array fields (LOAD_ARRAY)
+ */
+ public static void load(Beatmap beatmap, int flag) {
+ if (connection == null)
+ return;
+
+ try {
+ selectStmt.setString(1, beatmap.getFile().getParentFile().getName());
+ selectStmt.setString(2, beatmap.getFile().getName());
+ ResultSet rs = selectStmt.executeQuery();
+ if (rs.next()) {
+ if ((flag & LOAD_NONARRAY) > 0)
+ setBeatmapFields(rs, beatmap);
+ if ((flag & LOAD_ARRAY) > 0)
+ setBeatmapArrayFields(rs, beatmap);
+ }
+ rs.close();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to load Beatmap from database.", e, true);
+ }
+ }
+
+ /**
+ * Loads Beatmap fields from the database in a batch.
+ * @param batch a list of beatmaps
+ * @param flag whether to load all fields (LOAD_ALL), non-array
+ * fields (LOAD_NONARRAY), or array fields (LOAD_ARRAY)
+ */
+ public static void load(List batch, int flag) {
+ if (connection == null)
+ return;
+
+ // batch size too small
+ int size = batch.size();
+ if (size < cacheSize * LOAD_BATCH_MIN_RATIO) {
+ for (Beatmap beatmap : batch)
+ load(beatmap, flag);
+ return;
+ }
+
+ try (Statement stmt = connection.createStatement()) {
+ // create map
+ HashMap> map = new HashMap>();
+ for (Beatmap beatmap : batch) {
+ String parent = beatmap.getFile().getParentFile().getName();
+ String name = beatmap.getFile().getName();
+ HashMap m = map.get(parent);
+ if (m == null) {
+ m = new HashMap();
+ map.put(parent, m);
+ }
+ m.put(name, beatmap);
+ }
+
+ // iterate through database to load beatmaps
+ int count = 0;
+ stmt.setFetchSize(100);
+ String sql = "SELECT * FROM beatmaps";
+ ResultSet rs = stmt.executeQuery(sql);
+ while (rs.next()) {
+ String parent = rs.getString(1);
+ HashMap m = map.get(parent);
+ if (m != null) {
+ String name = rs.getString(2);
+ Beatmap beatmap = m.get(name);
+ if (beatmap != null) {
+ try {
+ if ((flag & LOAD_NONARRAY) > 0)
+ setBeatmapFields(rs, beatmap);
+ if ((flag & LOAD_ARRAY) > 0)
+ setBeatmapArrayFields(rs, beatmap);
+ } catch (SQLException e) {
+ Log.error(String.format("Failed to load map '%s/%s' from database.", parent, name), e);
+ }
+ if (++count >= size)
+ break;
+ }
+ }
+ }
+ rs.close();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to load beatmaps from database.", e, true);
+ }
+ }
+
+ /**
+ * Sets all beatmap non-array fields using a given result set.
+ * @param rs the result set containing the fields
+ * @param beatmap the beatmap
+ * @throws SQLException
+ */
+ private static void setBeatmapFields(ResultSet rs, Beatmap beatmap) throws SQLException {
+ try {
+ File dir = beatmap.getFile().getParentFile();
+ beatmap.beatmapID = rs.getInt(4);
+ beatmap.beatmapSetID = rs.getInt(5);
+ beatmap.title = BeatmapParser.getDBString(rs.getString(6));
+ beatmap.titleUnicode = BeatmapParser.getDBString(rs.getString(7));
+ beatmap.artist = BeatmapParser.getDBString(rs.getString(8));
+ beatmap.artistUnicode = BeatmapParser.getDBString(rs.getString(9));
+ beatmap.creator = BeatmapParser.getDBString(rs.getString(10));
+ beatmap.version = BeatmapParser.getDBString(rs.getString(11));
+ beatmap.source = BeatmapParser.getDBString(rs.getString(12));
+ beatmap.tags = BeatmapParser.getDBString(rs.getString(13));
+ beatmap.hitObjectCircle = rs.getInt(14);
+ beatmap.hitObjectSlider = rs.getInt(15);
+ beatmap.hitObjectSpinner = rs.getInt(16);
+ beatmap.HPDrainRate = rs.getFloat(17);
+ beatmap.circleSize = rs.getFloat(18);
+ beatmap.overallDifficulty = rs.getFloat(19);
+ beatmap.approachRate = rs.getFloat(20);
+ beatmap.sliderMultiplier = rs.getFloat(21);
+ beatmap.sliderTickRate = rs.getFloat(22);
+ beatmap.bpmMin = rs.getInt(23);
+ beatmap.bpmMax = rs.getInt(24);
+ beatmap.endTime = rs.getInt(25);
+ beatmap.audioFilename = new File(dir, BeatmapParser.getDBString(rs.getString(26)));
+ beatmap.audioLeadIn = rs.getInt(27);
+ beatmap.previewTime = rs.getInt(28);
+ beatmap.countdown = rs.getByte(29);
+ beatmap.sampleSet = BeatmapParser.getDBString(rs.getString(30));
+ beatmap.stackLeniency = rs.getFloat(31);
+ beatmap.mode = rs.getByte(32);
+ beatmap.letterboxInBreaks = rs.getBoolean(33);
+ beatmap.widescreenStoryboard = rs.getBoolean(34);
+ beatmap.epilepsyWarning = rs.getBoolean(35);
+ String bg = rs.getString(36);
+ if (bg != null)
+ beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
+ beatmap.sliderBorderFromString(rs.getString(37));
+ } catch (SQLException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SQLException(e);
+ }
+ }
+
+ /**
+ * Sets all Beatmap array fields using a given result set.
+ * @param rs the result set containing the fields
+ * @param beatmap the beatmap
+ * @throws SQLException
+ */
+ private static void setBeatmapArrayFields(ResultSet rs, Beatmap beatmap) throws SQLException {
+ try {
+ beatmap.timingPointsFromString(rs.getString(38));
+ beatmap.breaksFromString(rs.getString(39));
+ beatmap.comboFromString(rs.getString(40));
+ } catch (SQLException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SQLException(e);
+ }
+ }
+
+ /**
+ * Returns a map of file paths ({dir}/{file}) to last modified times, or
+ * null if any error occurred.
+ */
+ public static Map getLastModifiedMap() {
+ if (connection == null)
+ return null;
+
+ try (Statement stmt = connection.createStatement()) {
+ Map map = new HashMap();
+ String sql = "SELECT dir, file, lastModified FROM beatmaps";
+ ResultSet rs = stmt.executeQuery(sql);
+ stmt.setFetchSize(100);
+ while (rs.next()) {
+ String path = String.format("%s/%s", rs.getString(1), rs.getString(2));
+ long lastModified = rs.getLong(3);
+ map.put(path, lastModified);
+ }
+ rs.close();
+ return map;
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to get last modified map from database.", e, true);
+ return null;
+ }
+ }
+
+ /**
+ * Deletes the beatmap entry from the database.
+ * @param dir the directory
+ * @param file the file
+ */
+ public static void delete(String dir, String file) {
+ if (connection == null)
+ return;
+
+ try {
+ deleteMapStmt.setString(1, dir);
+ deleteMapStmt.setString(2, file);
+ cacheSize -= deleteMapStmt.executeUpdate();
+ updateCacheSize();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to delete beatmap entry from database.", e, true);
+ }
+ }
+
+ /**
+ * Deletes the beatmap group entry from the database.
+ * @param dir the directory
+ */
+ public static void delete(String dir) {
+ if (connection == null)
+ return;
+
+ try {
+ deleteGroupStmt.setString(1, dir);
+ cacheSize -= deleteGroupStmt.executeUpdate();
+ updateCacheSize();
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to delete beatmap group entry from database.", e, true);
+ }
+ }
+
+ /**
+ * Closes the connection to the database.
+ */
+ public static void closeConnection() {
+ if (connection == null)
+ return;
+
+ try {
+ insertStmt.close();
+ selectStmt.close();
+ deleteMapStmt.close();
+ deleteGroupStmt.close();
+ updateSizeStmt.close();
+ connection.close();
+ connection = null;
+ } catch (SQLException e) {
+ ErrorHandler.error("Failed to close beatmap database.", e, true);
+ }
+ }
+}
diff --git a/src/itdelatrisu/opsu/db/DBController.java b/src/itdelatrisu/opsu/db/DBController.java
index 05841b6c..aebd414b 100644
--- a/src/itdelatrisu/opsu/db/DBController.java
+++ b/src/itdelatrisu/opsu/db/DBController.java
@@ -43,7 +43,7 @@ public class DBController {
}
// initialize the databases
- OsuDB.init();
+ BeatmapDB.init();
ScoreDB.init();
}
@@ -51,7 +51,7 @@ public class DBController {
* Closes all database connections.
*/
public static void closeConnections() {
- OsuDB.closeConnection();
+ BeatmapDB.closeConnection();
ScoreDB.closeConnection();
}
diff --git a/src/itdelatrisu/opsu/db/OsuDB.java b/src/itdelatrisu/opsu/db/OsuDB.java
index f72bf4c5..d632daa3 100644
--- a/src/itdelatrisu/opsu/db/OsuDB.java
+++ b/src/itdelatrisu/opsu/db/OsuDB.java
@@ -1,3 +1,5 @@
+//todo rename
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java
index 73869809..4feee2da 100644
--- a/src/itdelatrisu/opsu/db/ScoreDB.java
+++ b/src/itdelatrisu/opsu/db/ScoreDB.java
@@ -20,8 +20,8 @@ package itdelatrisu.opsu.db;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.ScoreData;
+import itdelatrisu.opsu.beatmap.Beatmap;
import java.sql.Connection;
import java.sql.PreparedStatement;
@@ -249,18 +249,18 @@ public class ScoreDB {
/**
* Deletes all the scores for the given beatmap from the database.
- * @param osu the OsuFile object
+ * @param beatmap the beatmap
*/
- public static void deleteScore(OsuFile osu) {
+ public static void deleteScore(Beatmap beatmap) {
if (connection == null)
return;
try {
- deleteSongStmt.setInt(1, osu.beatmapID);
- deleteSongStmt.setString(2, osu.title);
- deleteSongStmt.setString(3, osu.artist);
- deleteSongStmt.setString(4, osu.creator);
- deleteSongStmt.setString(5, osu.version);
+ deleteSongStmt.setInt(1, beatmap.beatmapID);
+ deleteSongStmt.setString(2, beatmap.title);
+ deleteSongStmt.setString(3, beatmap.artist);
+ deleteSongStmt.setString(4, beatmap.creator);
+ deleteSongStmt.setString(5, beatmap.version);
deleteSongStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to delete scores from database.", e, true);
@@ -298,21 +298,21 @@ public class ScoreDB {
}
/**
- * Retrieves the game scores for an OsuFile map.
- * @param osu the OsuFile
+ * Retrieves the game scores for a beatmap.
+ * @param beatmap the beatmap
* @return all scores for the beatmap, or null if any error occurred
*/
- public static ScoreData[] getMapScores(OsuFile osu) {
+ public static ScoreData[] getMapScores(Beatmap beatmap) {
if (connection == null)
return null;
List list = new ArrayList();
try {
- selectMapStmt.setInt(1, osu.beatmapID);
- selectMapStmt.setString(2, osu.title);
- selectMapStmt.setString(3, osu.artist);
- selectMapStmt.setString(4, osu.creator);
- selectMapStmt.setString(5, osu.version);
+ selectMapStmt.setInt(1, beatmap.beatmapID);
+ selectMapStmt.setString(2, beatmap.title);
+ selectMapStmt.setString(3, beatmap.artist);
+ selectMapStmt.setString(4, beatmap.creator);
+ selectMapStmt.setString(5, beatmap.version);
ResultSet rs = selectMapStmt.executeQuery();
while (rs.next()) {
ScoreData s = new ScoreData(rs);
@@ -327,21 +327,21 @@ public class ScoreDB {
}
/**
- * Retrieves the game scores for an OsuFile map set.
- * @param osu the OsuFile
+ * Retrieves the game scores for a beatmap set.
+ * @param beatmap the beatmap
* @return all scores for the beatmap set (Version, ScoreData[]),
* or null if any error occurred
*/
- public static Map getMapSetScores(OsuFile osu) {
+ public static Map getMapSetScores(Beatmap beatmap) {
if (connection == null)
return null;
Map map = new HashMap();
try {
- selectMapSetStmt.setInt(1, osu.beatmapSetID);
- selectMapSetStmt.setString(2, osu.title);
- selectMapSetStmt.setString(3, osu.artist);
- selectMapSetStmt.setString(4, osu.creator);
+ selectMapSetStmt.setInt(1, beatmap.beatmapSetID);
+ selectMapSetStmt.setString(2, beatmap.title);
+ selectMapSetStmt.setString(3, beatmap.artist);
+ selectMapSetStmt.setString(4, beatmap.creator);
ResultSet rs = selectMapSetStmt.executeQuery();
List list = null;
diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java
index 0040aa2e..8ffa7dba 100644
--- a/src/itdelatrisu/opsu/downloads/DownloadNode.java
+++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java
@@ -21,11 +21,12 @@ package itdelatrisu.opsu.downloads;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.downloads.Download.Status;
+import itdelatrisu.opsu.downloads.servers.DownloadServer;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
@@ -85,7 +86,7 @@ public class DownloadNode {
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
- buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2f;
+ buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
buttonOffset = buttonHeight * 1.1f;
// download info
@@ -239,22 +240,26 @@ public class DownloadNode {
* @see #getDownload()
*/
public void createDownload(DownloadServer server) {
- if (download == null) {
- String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
- String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
- this.download = new Download(server.getURL(beatmapSetID), path, rename);
- download.setListener(new DownloadListener() {
- @Override
- public void completed() {
- UI.sendBarNotification(String.format("Download complete: %s", getTitle()));
- }
+ if (download != null)
+ return;
- @Override
- public void error() {
- UI.sendBarNotification("Download failed due to a connection error.");
- }
- });
- }
+ String url = server.getDownloadURL(beatmapSetID);
+ if (url == null)
+ return;
+ String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
+ String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
+ this.download = new Download(url, path, rename);
+ download.setListener(new DownloadListener() {
+ @Override
+ public void completed() {
+ UI.sendBarNotification(String.format("Download complete: %s", getTitle()));
+ }
+
+ @Override
+ public void error() {
+ UI.sendBarNotification("Download failed due to a connection error.");
+ }
+ });
}
/**
@@ -320,7 +325,7 @@ public class DownloadNode {
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// map is already loaded
- if (OsuGroupList.get().containsBeatmapSetID(beatmapSetID)) {
+ if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
g.setColor(Utils.COLOR_BLUE_BUTTON);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
}
@@ -340,13 +345,15 @@ public class DownloadNode {
textX += img.getWidth() + buttonWidth * 0.001f;
// text
- // TODO: if the title/artist line is too long, shorten it (e.g. add "...")
+ // TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
+ g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight());
Utils.FONT_BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
+ g.clearClip();
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
diff --git a/src/itdelatrisu/opsu/downloads/Updater.java b/src/itdelatrisu/opsu/downloads/Updater.java
index 5612e381..366fb623 100644
--- a/src/itdelatrisu/opsu/downloads/Updater.java
+++ b/src/itdelatrisu/opsu/downloads/Updater.java
@@ -20,14 +20,15 @@ package itdelatrisu.opsu.downloads;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.Download.DownloadListener;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
+import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -35,6 +36,7 @@ import java.util.Locale;
import java.util.Properties;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
@@ -206,7 +208,12 @@ public class Updater {
return;
// get latest version
- String s = Utils.readDataFromUrl(new URL(Options.VERSION_REMOTE));
+ String s = null;
+ try {
+ s = Utils.readDataFromUrl(new URL(Options.VERSION_REMOTE));
+ } catch (UnknownHostException e) {
+ Log.warn(String.format("Check for updates failed. Please check your internet connection, or your connection to %s.", Options.VERSION_REMOTE));
+ }
if (s == null) {
status = Status.CONNECTION_ERROR;
return;
diff --git a/src/itdelatrisu/opsu/downloads/BloodcatServer.java b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java
similarity index 68%
rename from src/itdelatrisu/opsu/downloads/BloodcatServer.java
rename to src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java
index 111cc77a..c696e800 100644
--- a/src/itdelatrisu/opsu/downloads/BloodcatServer.java
+++ b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java
@@ -16,25 +16,32 @@
* along with opsu!. If not, see .
*/
-package itdelatrisu.opsu.downloads;
+package itdelatrisu.opsu.downloads.servers;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
import org.json.JSONArray;
-import org.json.JSONException;
import org.json.JSONObject;
/**
* Download server: http://bloodcat.com/osu/
*/
public class BloodcatServer extends DownloadServer {
+ /** Server name. */
+ private static final String SERVER_NAME = "Bloodcat";
+
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d";
@@ -48,7 +55,10 @@ public class BloodcatServer extends DownloadServer {
public BloodcatServer() {}
@Override
- public String getURL(int beatmapSetID) {
+ public String getName() { return SERVER_NAME; }
+
+ @Override
+ public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@@ -58,7 +68,7 @@ public class BloodcatServer extends DownloadServer {
try {
// read JSON
String search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), rankedOnly ? "0" : "", page);
- JSONObject json = readJsonFromUrl(new URL(search));
+ JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
if (json == null) {
this.totalResults = -1;
return null;
@@ -70,7 +80,7 @@ public class BloodcatServer extends DownloadServer {
for (int i = 0; i < nodes.length; i++) {
JSONObject item = arr.getJSONObject(i);
nodes[i] = new DownloadNode(
- item.getInt("id"), item.getString("date"),
+ item.getInt("id"), formatDate(item.getString("date")),
item.getString("title"), item.isNull("titleUnicode") ? null : item.getString("titleUnicode"),
item.getString("artist"), item.isNull("artistUnicode") ? null : item.getString("artistUnicode"),
item.getString("creator")
@@ -85,25 +95,31 @@ public class BloodcatServer extends DownloadServer {
return nodes;
}
+ @Override
+ public int minQueryLength() { return 0; }
+
@Override
public int totalResults() { return totalResults; }
/**
- * Returns a JSON object from a URL.
- * @param url the remote URL
- * @return the JSON object
- * @author Roland Illig (http://stackoverflow.com/a/4308662)
+ * Returns a formatted date string from a raw date.
+ * @param s the raw date string (e.g. "2015-05-14T23:38:47+09:00")
+ * @return the formatted date, or the raw string if it could not be parsed
*/
- private static JSONObject readJsonFromUrl(URL url) throws IOException {
- String s = Utils.readDataFromUrl(url);
- JSONObject json = null;
- if (s != null) {
- try {
- json = new JSONObject(s);
- } catch (JSONException e) {
- ErrorHandler.error("Failed to create JSON object.", e, true);
- }
+ private String formatDate(String s) {
+ try {
+ // make string parseable by SimpleDateFormat
+ int index = s.lastIndexOf(':');
+ if (index == -1)
+ return s;
+ String str = new StringBuilder(s).deleteCharAt(index).toString();
+
+ DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+ Date d = f.parse(str);
+ DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
+ return fmt.format(d);
+ } catch (StringIndexOutOfBoundsException | ParseException e) {
+ return s;
}
- return json;
}
}
diff --git a/src/itdelatrisu/opsu/downloads/DownloadServer.java b/src/itdelatrisu/opsu/downloads/servers/DownloadServer.java
similarity index 80%
rename from src/itdelatrisu/opsu/downloads/DownloadServer.java
rename to src/itdelatrisu/opsu/downloads/servers/DownloadServer.java
index b3840364..24cca1f5 100644
--- a/src/itdelatrisu/opsu/downloads/DownloadServer.java
+++ b/src/itdelatrisu/opsu/downloads/servers/DownloadServer.java
@@ -16,7 +16,9 @@
* along with opsu!. If not, see .
*/
-package itdelatrisu.opsu.downloads;
+package itdelatrisu.opsu.downloads.servers;
+
+import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException;
@@ -27,12 +29,18 @@ public abstract class DownloadServer {
/** Track preview URL. */
private static final String PREVIEW_URL = "http://b.ppy.sh/preview/%d.mp3";
+ /**
+ * Returns the name of the download server.
+ * @return the server name
+ */
+ public abstract String getName();
+
/**
* Returns a web address to download the given beatmap.
* @param beatmapSetID the beatmap set ID
- * @return the URL string
+ * @return the URL string, or null if the address could not be determined
*/
- public abstract String getURL(int beatmapSetID);
+ public abstract String getDownloadURL(int beatmapSetID);
/**
* Returns a list of results for a given search query, or null if the
@@ -45,6 +53,12 @@ public abstract class DownloadServer {
*/
public abstract DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException;
+ /**
+ * Returns the minimum allowable length of a search query.
+ * @return the minimum length, or 0 if none
+ */
+ public abstract int minQueryLength();
+
/**
* Returns the total number of results for the last search query.
* This will differ from the the size of the array returned by
diff --git a/src/itdelatrisu/opsu/downloads/servers/HexideServer.java b/src/itdelatrisu/opsu/downloads/servers/HexideServer.java
new file mode 100644
index 00000000..1d31289f
--- /dev/null
+++ b/src/itdelatrisu/opsu/downloads/servers/HexideServer.java
@@ -0,0 +1,139 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.downloads.servers;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.downloads.DownloadNode;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Download server: https://osu.hexide.com/
+ */
+public class HexideServer extends DownloadServer {
+ /** Server name. */
+ private static final String SERVER_NAME = "Hexide";
+
+ /** Formatted download URL: {@code beatmapSetID,beatmapSetID} */
+ private static final String DOWNLOAD_URL = "https://osu.hexide.com/beatmaps/%d/download/%d.osz";
+
+ /** API fields. */
+ private static final String API_FIELDS = "maps.ranked_id;maps.title;maps.date;metadata.m_title;metadata.m_artist;metadata.m_creator";
+
+ /** Maximum beatmaps displayed per page. */
+ private static final int PAGE_LIMIT = 20;
+
+ /** Formatted home URL: {@code page} */
+ private static final String HOME_URL = "https://osu.hexide.com/search/" + API_FIELDS + "/maps.size.gt.0/order.date.desc/limit.%d." + (PAGE_LIMIT + 1);
+
+ /** Formatted search URL: {@code query,page} */
+ private static final String SEARCH_URL = "https://osu.hexide.com/search/" + API_FIELDS + "/maps.title.like.%s/order.date.desc/limit.%d." + (PAGE_LIMIT + 1);
+
+ /** Total result count from the last query. */
+ private int totalResults = -1;
+
+ /** Constructor. */
+ public HexideServer() {}
+
+ @Override
+ public String getName() { return SERVER_NAME; }
+
+ @Override
+ public String getDownloadURL(int beatmapSetID) {
+ return String.format(DOWNLOAD_URL, beatmapSetID, beatmapSetID);
+ }
+
+ @Override
+ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
+ DownloadNode[] nodes = null;
+ try {
+ // read JSON
+ int resultIndex = (page - 1) * PAGE_LIMIT;
+ String search;
+ if (query.isEmpty())
+ search = String.format(HOME_URL, resultIndex);
+ else
+ search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), resultIndex);
+ URL searchURL = new URL(search);
+ JSONArray arr = null;
+ try {
+ arr = Utils.readJsonArrayFromUrl(searchURL);
+ } catch (IOException e1) {
+ // a valid search with no results still throws an exception (?)
+ this.totalResults = 0;
+ return new DownloadNode[0];
+ }
+ if (arr == null) {
+ this.totalResults = -1;
+ return null;
+ }
+
+ // parse result list
+ nodes = new DownloadNode[Math.min(arr.length(), PAGE_LIMIT)];
+ for (int i = 0; i < nodes.length && i < PAGE_LIMIT; i++) {
+ JSONObject item = arr.getJSONObject(i);
+ String title, artist, creator;
+ if (item.has("versions")) {
+ JSONArray versions = item.getJSONArray("versions");
+ JSONObject version = versions.getJSONObject(0);
+ title = version.getString("m_title");
+ artist = version.getString("m_artist");
+ creator = version.getString("m_creator");
+ } else { // "versions" is sometimes missing (?)
+ String str = item.getString("title");
+ int index = str.indexOf(" - ");
+ if (index > -1) {
+ title = str.substring(0, index);
+ artist = str.substring(index + 3);
+ creator = "?";
+ } else { // should never happen...
+ title = str;
+ artist = creator = "?";
+ }
+ }
+ nodes[i] = new DownloadNode(
+ item.getInt("ranked_id"), item.getString("date"),
+ title, null, artist, null, creator
+ );
+ }
+
+ // store total result count
+ // NOTE: The API doesn't provide a result count without retrieving
+ // all results at once; this approach just gets pagination correct.
+ this.totalResults = arr.length() + resultIndex;
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
+ }
+ return nodes;
+ }
+
+ @Override
+ public int minQueryLength() { return 0; }
+
+ @Override
+ public int totalResults() { return totalResults; }
+}
diff --git a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
new file mode 100644
index 00000000..d7179664
--- /dev/null
+++ b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
@@ -0,0 +1,151 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.downloads.servers;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.downloads.DownloadNode;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Download server: http://loli.al/
+ */
+public class OsuMirrorServer extends DownloadServer {
+ /** Server name. */
+ private static final String SERVER_NAME = "osu!Mirror";
+
+ /** Formatted download URL: {@code beatmapSetID} */
+ private static final String DOWNLOAD_URL = "http://loli.al/d/%d/";
+
+ /** Formatted search URL: {@code page,query} */
+ private static final String SEARCH_URL = "http://loli.al/mirror/search/%d.json?keyword=%s";
+
+ /** Formatted home URL: {@code page} */
+ private static final String HOME_URL = "http://loli.al/mirror/home/%d.json";
+
+ /** Minimum allowable length of a search query. */
+ private static final int MIN_QUERY_LENGTH = 3;
+
+ /** Total result count from the last query. */
+ private int totalResults = -1;
+
+ /** Max server download ID seen (for approximating total pages). */
+ private int maxServerID = 0;
+
+ /** Lookup table from beatmap set ID -> server download ID. */
+ private HashMap idTable = new HashMap();
+
+ /** Constructor. */
+ public OsuMirrorServer() {}
+
+ @Override
+ public String getName() { return SERVER_NAME; }
+
+ @Override
+ public String getDownloadURL(int beatmapSetID) {
+ return (idTable.containsKey(beatmapSetID)) ? String.format(DOWNLOAD_URL, idTable.get(beatmapSetID)) : null;
+ }
+
+ @Override
+ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
+ // NOTE: ignores 'rankedOnly' flag.
+ DownloadNode[] nodes = null;
+ try {
+ // read JSON
+ String search;
+ boolean isSearch;
+ if (query.isEmpty()) {
+ isSearch = false;
+ search = String.format(HOME_URL, page);
+ } else {
+ isSearch = true;
+ search = String.format(SEARCH_URL, page, URLEncoder.encode(query, "UTF-8"));
+ }
+ JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
+ if (json == null || json.getInt("code") != 0) {
+ this.totalResults = -1;
+ return null;
+ }
+
+ // parse result list
+ JSONArray arr = json.getJSONArray("maplist");
+ nodes = new DownloadNode[arr.length()];
+ for (int i = 0; i < nodes.length; i++) {
+ JSONObject item = arr.getJSONObject(i);
+ int beatmapSetID = item.getInt("OSUSetid");
+ int serverID = item.getInt("id");
+ nodes[i] = new DownloadNode(
+ beatmapSetID, formatDate(item.getString("ModifyDate")),
+ item.getString("Title"), null,
+ item.getString("Artist"), null,
+ item.getString("Mapper")
+ );
+ idTable.put(beatmapSetID, serverID);
+ if (serverID > maxServerID)
+ maxServerID = serverID;
+ }
+
+ // store total result count
+ if (isSearch)
+ this.totalResults = json.getInt("totalRows");
+ else
+ this.totalResults = maxServerID;
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
+ }
+ return nodes;
+ }
+
+ @Override
+ public int minQueryLength() { return MIN_QUERY_LENGTH; }
+
+ @Override
+ public int totalResults() { return totalResults; }
+
+ /**
+ * Returns a formatted date string from a raw date.
+ * @param s the raw date string (e.g. "2015-05-14T23:38:47Z")
+ * @return the formatted date, or the raw string if it could not be parsed
+ */
+ private String formatDate(String s) {
+ try {
+ DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ f.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Date d = f.parse(s);
+ DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
+ return fmt.format(d);
+ } catch (ParseException e) {
+ return s;
+ }
+ }
+}
diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java
index 85f367da..cb9bada1 100644
--- a/src/itdelatrisu/opsu/objects/Circle.java
+++ b/src/itdelatrisu/opsu/objects/Circle.java
@@ -19,10 +19,12 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData;
+import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.states.Game;
import org.newdawn.slick.Color;
@@ -32,14 +34,14 @@ import org.newdawn.slick.Graphics;
/**
* Data type representing a circle object.
*/
-public class Circle implements HitObject {
+public class Circle implements GameObject {
/** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375;
private static float diameter;
- /** The associated OsuHitObject. */
- private OsuHitObject hitObject;
+ /** The associated HitObject. */
+ private HitObject hitObject;
/** The scaled starting x, y coordinates. */
private float x, y;
@@ -69,17 +71,22 @@ public class Circle implements HitObject {
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
+ /*int diameter = (int) (104 - (circleSize * 8));
+ diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter));
+ GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter));
+ GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter));*/
}
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param game the associated Game object
* @param data the associated GameData object
* @param color the color of this circle
* @param comboEnd true if this is the last hit object in the combo
*/
- public Circle(OsuHitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
+ public Circle(HitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
this.hitObject = hitObject;
this.game = game;
this.data = data;
@@ -102,9 +109,13 @@ public class Circle implements HitObject {
if (timeDiff >= 0)
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
- GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
+ boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
+ if (!overlayAboveNumber)
+ GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
+ if (overlayAboveNumber)
+ GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
Utils.COLOR_WHITE_FADE.a = oldAlpha;
}
@@ -144,7 +155,7 @@ public class Circle implements HitObject {
if (result > -1) {
data.addHitError(hitObject.getTime(), x, y, timeDiff);
- data.hitResult(hitObject.getTime(), result, this.x, this.y, color, comboEnd, hitObject, 0);
+ data.hitResult(trackPosition, result, this.x, this.y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
return true;
}
}
@@ -160,17 +171,17 @@ public class Circle implements HitObject {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) // "auto" mod: catch any missed notes due to lag
- data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0);
+ data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
else // no more points can be scored, so send a miss
- data.hitResult(time, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0);
+ data.hitResult(trackPosition, GameData.HIT_MISS, x, y, null, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
return true;
}
// "auto" mod: send a perfect hit result
else if (isAutoMod) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
- data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0);
+ data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
return true;
}
}
diff --git a/src/itdelatrisu/opsu/objects/DummyObject.java b/src/itdelatrisu/opsu/objects/DummyObject.java
index 4f727345..80f920a7 100644
--- a/src/itdelatrisu/opsu/objects/DummyObject.java
+++ b/src/itdelatrisu/opsu/objects/DummyObject.java
@@ -18,25 +18,25 @@
package itdelatrisu.opsu.objects;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.beatmap.HitObject;
import org.newdawn.slick.Graphics;
/**
- * Dummy hit object, used when another HitObject class cannot be created.
+ * Dummy hit object, used when another GameObject class cannot be created.
*/
-public class DummyObject implements HitObject {
- /** The associated OsuHitObject. */
- private OsuHitObject hitObject;
+public class DummyObject implements GameObject {
+ /** The associated HitObject. */
+ private HitObject hitObject;
/** The scaled starting x, y coordinates. */
private float x, y;
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
*/
- public DummyObject(OsuHitObject hitObject) {
+ public DummyObject(HitObject hitObject) {
this.hitObject = hitObject;
updatePosition();
}
diff --git a/src/itdelatrisu/opsu/objects/GameObject.java b/src/itdelatrisu/opsu/objects/GameObject.java
new file mode 100644
index 00000000..ca1ab8e5
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/GameObject.java
@@ -0,0 +1,72 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.objects;
+
+import org.newdawn.slick.Graphics;
+
+/**
+ * Interface for hit object types used during gameplay.
+ */
+public interface GameObject {
+ /**
+ * Draws the hit object to the graphics context.
+ * @param g the graphics context
+ * @param trackPosition the current track position
+ */
+ public void draw(Graphics g, int trackPosition);
+
+ /**
+ * Updates the hit object.
+ * @param overlap true if the next object's start time has already passed
+ * @param delta the delta interval since the last call
+ * @param mouseX the x coordinate of the mouse
+ * @param mouseY the y coordinate of the mouse
+ * @param keyPressed whether or not a game key is currently pressed
+ * @param trackPosition the track position
+ * @return true if object ended
+ */
+ public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition);
+
+ /**
+ * Processes a mouse click.
+ * @param x the x coordinate of the mouse
+ * @param y the y coordinate of the mouse
+ * @param trackPosition the track position
+ * @return true if a hit result was processed
+ */
+ public boolean mousePressed(int x, int y, int trackPosition);
+
+ /**
+ * Returns the coordinates of the hit object at a given track position.
+ * @param trackPosition the track position
+ * @return the [x,y] coordinates
+ */
+ public float[] getPointAt(int trackPosition);
+
+ /**
+ * Returns the end time of the hit object.
+ * @return the end time, in milliseconds
+ */
+ public int getEndTime();
+
+ /**
+ * Updates the position of the hit object.
+ */
+ public void updatePosition();
+}
diff --git a/src/itdelatrisu/opsu/objects/HitObject.java b/src/itdelatrisu/opsu/objects/HitObject.java
index 0be6c3e0..d4602e2d 100644
--- a/src/itdelatrisu/opsu/objects/HitObject.java
+++ b/src/itdelatrisu/opsu/objects/HitObject.java
@@ -1,3 +1,5 @@
+//TODO rename
+
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java
index fb88aa11..ac50e429 100644
--- a/src/itdelatrisu/opsu/objects/Slider.java
+++ b/src/itdelatrisu/opsu/objects/Slider.java
@@ -19,11 +19,14 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData;
+import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
@@ -37,7 +40,7 @@ import org.newdawn.slick.Image;
/**
* Data type representing a slider object.
*/
-public class Slider implements HitObject {
+public class Slider implements GameObject {
/** Slider ball frames. */
private static Image[] sliderBallImages;
@@ -54,8 +57,8 @@ public class Slider implements HitObject {
/** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375;
- /** The associated OsuHitObject. */
- private OsuHitObject hitObject;
+ /** The associated HitObject. */
+ private HitObject hitObject;
/** The scaled starting x, y coordinates. */
protected float x, y;
@@ -108,9 +111,9 @@ public class Slider implements HitObject {
* Initializes the Slider data type with images and dimensions.
* @param container the game container
* @param circleSize the map's circleSize value
- * @param osu the associated OsuFile object
+ * @param beatmap the associated beatmap
*/
- public static void init(GameContainer container, float circleSize, OsuFile osu) {
+ public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
@@ -119,7 +122,11 @@ public class Slider implements HitObject {
int diameterInt = (int)diameter;
followRadius = diameter / 2 * 3f;
-
+ //TODO conflict
+ /*
+ int diameter = (int) (104 - (circleSize * 8));
+ diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ */
// slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
@@ -133,19 +140,19 @@ public class Slider implements HitObject {
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4));
- sliderMultiplier = osu.sliderMultiplier;
- sliderTickRate = osu.sliderTickRate;
+ sliderMultiplier = beatmap.sliderMultiplier;
+ sliderTickRate = beatmap.sliderTickRate;
}
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param game the associated Game object
* @param data the associated GameData object
* @param color the color of this slider
* @param comboEnd true if this is the last hit object in the combo
*/
- public Slider(OsuHitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
+ public Slider(HitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
this.hitObject = hitObject;
this.game = game;
this.data = data;
@@ -176,12 +183,25 @@ public class Slider implements HitObject {
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
float approachScale = 1 + scale * 3;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
+ boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
+ Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
+ Image hitCircle = GameImage.HITCIRCLE.getImage();
+ float[] endPos = curve.pointAt(1);
- // curve
- curve.draw();
+ curve.draw(color);
+ color.a = alpha;
+
+ // end circle
+ hitCircle.drawCentered(endPos[0], endPos[1], color);
+ hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
+
+ // start circle
+ hitCircle.drawCentered(x, y, color);
+ if (!overlayAboveNumber)
+ hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
// ticks
if (ticksT != null) {
@@ -191,23 +211,13 @@ public class Slider implements HitObject {
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
}
}
-
- Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
- Image hitCircle = GameImage.HITCIRCLE.getImage();
-
- // end circle
- float[] endPos = curve.pointAt(1);
- hitCircle.drawCentered(endPos[0], endPos[1], color);
- hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
-
- // start circle
- hitCircle.drawCentered(x, y, color);
- hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
if (sliderClickedInitial)
; // don't draw current combo number if already clicked
else
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
- hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
+ hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
+ if (overlayAboveNumber)
+ hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
// repeats
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
@@ -245,7 +255,7 @@ public class Slider implements HitObject {
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
float t = getT(trackPosition, false);
-// float dis = hitObject.getPixelLength() * OsuHitObject.getXMultiplier() * (t - (int) t);
+// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
@@ -336,14 +346,20 @@ public class Slider implements HitObject {
else
result = GameData.HIT_MISS;
+ float cx, cy;
+ HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle
float[] lastPos = curve.pointAt(1);
- data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
- lastPos[0], lastPos[1], color, comboEnd, hitObject, currentRepeats + 1);
+ cx = lastPos[0];
+ cy = lastPos[1];
+ type = HitObjectType.SLIDER_LAST;
} else { // first circle
- data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
- x, y, color, comboEnd, hitObject, currentRepeats + 1);
+ cx = x;
+ cy = y;
+ type = HitObjectType.SLIDER_FIRST;
}
+ data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
+ cx, cy, color, comboEnd, hitObject, currentRepeats + 1, type, curve, sliderClickedFinal);
return result;
}
@@ -509,10 +525,12 @@ public class Slider implements HitObject {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
- if (hitObject.getSliderType() == OsuHitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
+ if (hitObject.getSliderType() == HitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
this.curve = new CircumscribedCircle(hitObject, color);
+ else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
+ this.curve = new CatmullCurve(hitObject, color);
else
- this.curve = new LinearBezier(hitObject, color);
+ this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
}
@Override
diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java
index acc8c28d..c8298ff8 100644
--- a/src/itdelatrisu/opsu/objects/Spinner.java
+++ b/src/itdelatrisu/opsu/objects/Spinner.java
@@ -19,12 +19,14 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData;
+import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.states.Game;
import org.newdawn.slick.Color;
@@ -35,7 +37,7 @@ import org.newdawn.slick.Image;
/**
* Data type representing a spinner object.
*/
-public class Spinner implements HitObject {
+public class Spinner implements GameObject {
/** Container dimensions. */
private static int width, height;
@@ -62,8 +64,8 @@ public class Spinner implements HitObject {
private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * 477 / 60 / 1000 * TWO_PI; // ~95.3
- /** The associated OsuHitObject. */
- private OsuHitObject hitObject;
+ /** The associated HitObject. */
+ private HitObject hitObject;
/** The associated GameData object. */
private GameData data;
@@ -110,11 +112,11 @@ public class Spinner implements HitObject {
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param game the associated Game object
* @param data the associated GameData object
*/
- public Spinner(OsuHitObject hitObject, Game game, GameData data) {
+ public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
this.data = data;
@@ -132,15 +134,17 @@ public class Spinner implements HitObject {
return;
boolean spinnerComplete = (rotations >= rotationsNeeded);
+ float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
// darken screen
- float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
- float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
- if (timeDiff > 0)
- Utils.COLOR_BLACK_ALPHA.a *= alpha;
- g.setColor(Utils.COLOR_BLACK_ALPHA);
- g.fillRect(0, 0, width, height);
- Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ if (Options.getSkin().isSpinnerFadePlayfield()) {
+ float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
+ if (timeDiff > 0)
+ Utils.COLOR_BLACK_ALPHA.a *= alpha;
+ g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.fillRect(0, 0, width, height);
+ Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ }
// rpm
Image rpmImg = GameImage.SPINNER_RPM.getImage();
@@ -198,7 +202,7 @@ public class Spinner implements HitObject {
result = GameData.HIT_MISS;
data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
- Color.transparent, true, hitObject, 0);
+ Color.transparent, true, hitObject, 0, HitObjectType.SPINNER, null, true);
return result;
}
diff --git a/src/itdelatrisu/opsu/objects/curves/Bezier2.java b/src/itdelatrisu/opsu/objects/curves/Bezier2.java
index 35552b4a..df0a5e09 100644
--- a/src/itdelatrisu/opsu/objects/curves/Bezier2.java
+++ b/src/itdelatrisu/opsu/objects/curves/Bezier2.java
@@ -23,22 +23,10 @@ package itdelatrisu.opsu.objects.curves;
*
* @author fluddokt (https://github.com/fluddokt)
*/
-public class Bezier2 {
+public class Bezier2 extends CurveType {
/** The control points of the Bezier curve. */
private Vec2f[] points;
- /** Points along the curve of the Bezier curve. */
- private Vec2f[] curve;
-
- /** Distances between a point of the curve and the last point. */
- private float[] curveDis;
-
- /** The number of points along the curve. */
- private int ncurve;
-
- /** The total distances of this Bezier. */
- private float totalDistance;
-
/**
* Constructor.
* @param points the control points
@@ -52,27 +40,10 @@ public class Bezier2 {
for (int i = 0; i < points.length - 1; i++)
approxlength += points[i].cpy().sub(points[i + 1]).len();
- // subdivide the curve
- this.ncurve = (int) (approxlength / 4);
- this.curve = new Vec2f[ncurve];
- for (int i = 0; i < ncurve; i++)
- curve[i] = pointAt(i / (float) ncurve);
-
- // find the distance of each point from the previous point
- this.curveDis = new float[ncurve];
- this.totalDistance = 0;
- for (int i = 0; i < ncurve; i++) {
- curveDis[i] = (i == 0) ? 0 : curve[i].cpy().sub(curve[i - 1]).len();
- totalDistance += curveDis[i];
- }
-// System.out.println("New Bezier2 "+points.length+" "+approxlength+" "+totalDistance());
+ init(approxlength);
}
- /**
- * Returns the point on the Bezier curve at a value t.
- * @param t the t value [0, 1]
- * @return the point [x, y]
- */
+ @Override
public Vec2f pointAt(float t) {
Vec2f c = new Vec2f();
int n = points.length - 1;
@@ -84,31 +55,11 @@ public class Bezier2 {
return c;
}
- /**
- * Returns the points along the curve of the Bezier curve.
- */
- public Vec2f[] getCurve() { return curve; }
-
- /**
- * Returns the distances between a point of the curve and the last point.
- */
- public float[] getCurveDistances() { return curveDis; }
-
- /**
- * Returns the number of points along the curve.
- */
- public int points() { return ncurve; }
-
- /**
- * Returns the total distances of this Bezier curve.
- */
- public float totalDistance() { return totalDistance; }
-
/**
* Calculates the binomial coefficient.
* http://en.wikipedia.org/wiki/Binomial_coefficient#Binomial_coefficient_in_programming_languages
*/
- public static long binomialCoefficient(int n, int k) {
+ private static long binomialCoefficient(int n, int k) {
if (k < 0 || k > n)
return 0;
if (k == 0 || k == n)
diff --git a/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
new file mode 100644
index 00000000..95842e77
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
@@ -0,0 +1,77 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.objects.curves;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.beatmap.HitObject;
+
+import java.util.LinkedList;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.SlickException;
+
+/**
+ * Representation of Catmull Curve with equidistant points.
+ *
+ * @author fluddokt (https://github.com/fluddokt)
+ */
+public class CatmullCurve extends EqualDistanceMultiCurve {
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param color the color of this curve
+ */
+ public CatmullCurve(HitObject hitObject, Color color) {
+ super(hitObject, color);
+ LinkedList catmulls = new LinkedList();
+ int ncontrolPoints = hitObject.getSliderX().length + 1;
+ LinkedList points = new LinkedList(); // temporary list of points to separate different curves
+
+ // repeat the first and last points as controls points
+ // only if the first/last two points are different
+ // aabb
+ // aabc abcc
+ // aabc abcd bcdd
+ if (getX(0) != getX(1) || getY(0) != getY(1))
+ points.addLast(new Vec2f(getX(0), getY(0)));
+ for (int i = 0; i < ncontrolPoints; i++) {
+ points.addLast(new Vec2f(getX(i), getY(i)));
+ if (points.size() >= 4) {
+ try {
+ catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
+ } catch (SlickException e) {
+ ErrorHandler.error(null, e, true);
+ }
+ points.removeFirst();
+ }
+ }
+ if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2)
+ ||getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
+ points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
+ if (points.size() >= 4) {
+ try {
+ catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
+ } catch (SlickException e) {
+ ErrorHandler.error(null, e, true);
+ }
+ }
+
+ init(catmulls);
+ }
+}
diff --git a/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java b/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
new file mode 100644
index 00000000..5a13d7ce
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
@@ -0,0 +1,85 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.objects.curves;
+
+import org.newdawn.slick.SlickException;
+
+/**
+ * Representation of a Centripetal Catmull–Rom spline.
+ * (Currently not technically Centripetal Catmull–Rom.)
+ * http://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
+ *
+ * @author fluddokt (https://github.com/fluddokt)
+ */
+public class CentripetalCatmullRom extends CurveType {
+ /** The time values of the Catmull curve. */
+ private float [] time;
+
+ /** The control points of the Catmull curve. */
+ private Vec2f[] points;
+
+ /**
+ * Constructor.
+ * @param points the control points of the curve
+ * @throws SlickException
+ */
+ protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
+ if (points.length != 4)
+ throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
+
+ this.points = points;
+ time = new float[4];
+ time[0] = 0;
+ float approxLength = 0;
+ for (int i = 1; i < 4; i++) {
+ float len = 0;
+ if (i > 0)
+ len = points[i].cpy().sub(points[i - 1]).len();
+ if (len <= 0)
+ len += 0.0001f;
+ approxLength += len;
+ // time[i] = (float) Math.sqrt(len) + time[i-1];// ^(0.5)
+ time[i] = i;
+ }
+
+ init(approxLength / 2);
+ }
+
+ @Override
+ public Vec2f pointAt(float t) {
+ t = t * (time[2] - time[1]) + time[1];
+
+ Vec2f A1 = points[0].cpy().scale((time[1] - t) / (time[1] - time[0]))
+ .add(points[1].cpy().scale((t - time[0]) / (time[1] - time[0])));
+ Vec2f A2 = points[1].cpy().scale((time[2] - t) / (time[2] - time[1]))
+ .add(points[2].cpy().scale((t - time[1]) / (time[2] - time[1])));
+ Vec2f A3 = points[2].cpy().scale((time[3] - t) / (time[3] - time[2]))
+ .add(points[3].cpy().scale((t - time[2]) / (time[3] - time[2])));
+
+ Vec2f B1 = A1.cpy().scale((time[2] - t) / (time[2] - time[0]))
+ .add(A2.cpy().scale((t - time[0]) / (time[2] - time[0])));
+ Vec2f B2 = A2.cpy().scale((time[3] - t) / (time[3] - time[1]))
+ .add(A3.cpy().scale((t - time[1]) / (time[3] - time[1])));
+
+ Vec2f C = B1.cpy().scale((time[2] - t) / (time[2] - time[1]))
+ .add(B2.cpy().scale((t - time[1]) / (time[2] - time[1])));
+
+ return C;
+ }
+}
diff --git a/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java b/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
index 74ac354d..b62c17af 100644
--- a/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
+++ b/src/itdelatrisu/opsu/objects/curves/CircumscribedCircle.java
@@ -19,12 +19,9 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler;
-import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.OsuHitObject;
-import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
import org.newdawn.slick.Color;
-import org.newdawn.slick.Image;
/**
* Representation of a curve along a Circumscribed Circle of three points.
@@ -53,22 +50,14 @@ public class CircumscribedCircle extends Curve {
/** The start and end angles for drawing. */
private float drawStartAngle, drawEndAngle;
- /** The number of steps in the curve to draw. */
- private float step;
-
- /** Points along the curve. */
- private Vec2f[] curve;
-
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param color the color of this curve
*/
- public CircumscribedCircle(OsuHitObject hitObject, Color color) {
+ public CircumscribedCircle(HitObject hitObject, Color color) {
super(hitObject, color);
- this.step = hitObject.getPixelLength() / 5f;
-
// construct the three points
this.start = new Vec2f(getX(0), getY(0));
this.mid = new Vec2f(getX(1), getY(1));
@@ -114,7 +103,7 @@ public class CircumscribedCircle extends Curve {
// find an angle with an arc length of pixelLength along this circle
this.radius = startAngPoint.len();
- float pixelLength = hitObject.getPixelLength() * OsuHitObject.getXMultiplier();
+ float pixelLength = hitObject.getPixelLength() * HitObject.getXMultiplier();
float arcAng = pixelLength / radius; // len = theta * r / theta = len / r
// now use it for our new end angle
@@ -125,6 +114,7 @@ public class CircumscribedCircle extends Curve {
this.drawStartAngle = (float) ((startAng + (startAng > endAng ? -HALF_PI : HALF_PI)) * 180 / Math.PI);
// calculate points
+ float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
curve = new Vec2f[(int) step + 1];
for (int i = 0; i < curve.length; i++) {
float[] xy = pointAt(i / step);
@@ -178,16 +168,6 @@ public class CircumscribedCircle extends Curve {
};
}
- @Override
- public void draw() {
- Image hitCircle = GameImage.HITCIRCLE.getImage();
- Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
- for (int i = 0; i < step; i++)
- hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
- for (int i = 0; i < step; i++)
- hitCircle.drawCentered(curve[i].x, curve[i].y, color);
- }
-
@Override
public float getEndAngle() { return drawEndAngle; }
diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java
index 028923be..1ae526ab 100644
--- a/src/itdelatrisu/opsu/objects/curves/Curve.java
+++ b/src/itdelatrisu/opsu/objects/curves/Curve.java
@@ -18,9 +18,17 @@
package itdelatrisu.opsu.objects.curves;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Options;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.render.CurveRenderState;
+import itdelatrisu.opsu.skins.Skin;
+import org.lwjgl.opengl.GLContext;
import org.newdawn.slick.Color;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.util.Log;
/**
* Representation of a curve.
@@ -28,11 +36,17 @@ import org.newdawn.slick.Color;
* @author fluddokt (https://github.com/fluddokt)
*/
public abstract class Curve {
- /** The associated OsuHitObject. */
- protected OsuHitObject hitObject;
+ /** Points generated along the curve should be spaced this far apart. */
+ protected static float CURVE_POINTS_SEPERATION = 5;
- /** The color of this curve. */
- protected Color color;
+ /** The curve border color. */
+ private static Color borderColor;
+
+ /** Whether OpenGL 3.0 is supported. */
+ private static boolean openGL30 = false;
+
+ /** The associated HitObject. */
+ protected HitObject hitObject;
/** The scaled starting x, y coordinates. */
protected float x, y;
@@ -40,18 +54,43 @@ public abstract class Curve {
/** The scaled slider x, y coordinate lists. */
protected float[] sliderX, sliderY;
+ /** Per-curve render-state used for the new style curve renders. */
+ private CurveRenderState renderState;
+
+ /** Points along the curve (set by inherited classes). */
+ protected Vec2f[] curve;
+
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param color the color of this curve
*/
- protected Curve(OsuHitObject hitObject, Color color) {
+ protected Curve(HitObject hitObject, Color color) {
this.hitObject = hitObject;
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
- this.color = color;
+ this.renderState = null;
+ }
+
+ /**
+ * Set the width and height of the container that Curves get drawn into.
+ * Should be called before any curves are drawn.
+ * @param width the container width
+ * @param height the container height
+ * @param circleSize the circle size
+ * @param borderColor the curve border color
+ */
+ public static void init(int width, int height, float circleSize, Color borderColor) {
+ Curve.borderColor = borderColor;
+ openGL30 = GLContext.getCapabilities().OpenGL30;
+ if (openGL30)
+ CurveRenderState.init(width, height, circleSize);
+ else {
+ if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
+ Log.warn("New slider style requires OpenGL 3.0, which is not supported.");
+ }
}
/**
@@ -63,8 +102,29 @@ public abstract class Curve {
/**
* Draws the full curve to the graphics context.
+ * @param color the color filter
*/
- public abstract void draw();
+ public void draw(Color color) {
+ if (curve == null)
+ return;
+
+ // peppysliders
+ if (Options.getSkin().getSliderStyle() == Skin.STYLE_PEPPYSLIDER || !openGL30) {
+ Image hitCircle = GameImage.HITCIRCLE.getImage();
+ Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
+ for (int i = 0; i < curve.length; i++)
+ hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
+ for (int i = 0; i < curve.length; i++)
+ hitCircle.drawCentered(curve[i].x, curve[i].y, color);
+ }
+
+ // mmsliders
+ else {
+ if (renderState == null)
+ renderState = new CurveRenderState(hitObject);
+ renderState.draw(color, borderColor, curve);
+ }
+ }
/**
* Returns the angle of the first control point.
@@ -94,4 +154,12 @@ public abstract class Curve {
protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
+
+ /**
+ * Discards the slider cache (only used for mmsliders).
+ */
+ public void discardCache() {
+ if (renderState != null)
+ renderState.discardCache();
+ }
}
diff --git a/src/itdelatrisu/opsu/objects/curves/CurveType.java b/src/itdelatrisu/opsu/objects/curves/CurveType.java
new file mode 100644
index 00000000..4af02a62
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/curves/CurveType.java
@@ -0,0 +1,86 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.objects.curves;
+
+/**
+ * Representation of a curve with the distance between each point calculated.
+ *
+ * @author fluddokt (https://github.com/fluddokt)
+ */
+public abstract class CurveType {
+ /** Points along the curve of the Bezier curve. */
+ private Vec2f[] curve;
+
+ /** Distances between a point of the curve and the last point. */
+ private float[] curveDis;
+
+ /** The number of points along the curve. */
+ private int ncurve;
+
+ /** The total distances of this Bezier. */
+ private float totalDistance;
+
+ /**
+ * Returns the point on the curve at a value t.
+ * @param t the t value [0, 1]
+ * @return the point [x, y]
+ */
+ public abstract Vec2f pointAt(float t);
+
+ /**
+ * Initialize the curve points and distance.
+ * Must be called by inherited classes.
+ * @param approxlength an approximate length of the curve
+ */
+ public void init(float approxlength) {
+ // subdivide the curve
+ this.ncurve = (int) (approxlength / 4) + 2;
+ this.curve = new Vec2f[ncurve];
+ for (int i = 0; i < ncurve; i++)
+ curve[i] = pointAt(i / (float) (ncurve - 1));
+
+ // find the distance of each point from the previous point
+ this.curveDis = new float[ncurve];
+ this.totalDistance = 0;
+ for (int i = 0; i < ncurve; i++) {
+ curveDis[i] = (i == 0) ? 0 : curve[i].cpy().sub(curve[i - 1]).len();
+ totalDistance += curveDis[i];
+ }
+ }
+
+ /**
+ * Returns the points along the curve of the Bezier curve.
+ */
+ public Vec2f[] getCurvePoint() { return curve; }
+
+ /**
+ * Returns the distances between a point of the curve and the last point.
+ */
+ public float[] getCurveDistances() { return curveDis; }
+
+ /**
+ * Returns the number of points along the curve.
+ */
+ public int getCurvesCount() { return ncurve; }
+
+ /**
+ * Returns the total distances of this Bezier curve.
+ */
+ public float totalDistance() { return totalDistance; }
+}
diff --git a/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java b/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
new file mode 100644
index 00000000..a15a1657
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
@@ -0,0 +1,142 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.objects.curves;
+
+import itdelatrisu.opsu.beatmap.HitObject;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import org.newdawn.slick.Color;
+
+/**
+ * Representation of multiple curve with equidistant points.
+ * http://pomax.github.io/bezierinfo/#tracing
+ *
+ * @author fluddokt (https://github.com/fluddokt)
+ */
+public abstract class EqualDistanceMultiCurve extends Curve {
+ /** The angles of the first and last control points for drawing. */
+ private float startAngle, endAngle;
+
+ /** The number of points along the curve. */
+ private int ncurve;
+
+ /**
+ * Constructor.
+ * @param hitObject the associated HitObject
+ * @param color the color of this curve
+ */
+ public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
+ super(hitObject, color);
+ }
+
+ /**
+ * Initialize the curve points with equal distance.
+ * Must be called by inherited classes.
+ * @param curvesList a list of curves to join
+ */
+ public void init(LinkedList curvesList){
+ // now try to creates points the are equidistant to each other
+ this.ncurve = (int) (hitObject.getPixelLength() / CURVE_POINTS_SEPERATION);
+ this.curve = new Vec2f[ncurve + 1];
+
+ float distanceAt = 0;
+ Iterator iter = curvesList.iterator();
+ int curPoint = 0;
+ CurveType curCurve = iter.next();
+ Vec2f lastCurve = curCurve.getCurvePoint()[0];
+ float lastDistanceAt = 0;
+
+ // length of Curve should equal pixel length (in 640x480)
+ float pixelLength = hitObject.getPixelLength() * HitObject.getXMultiplier();
+
+ // for each distance, try to get in between the two points that are between it
+ for (int i = 0; i < ncurve + 1; i++) {
+ int prefDistance = (int) (i * pixelLength / ncurve);
+ while (distanceAt < prefDistance) {
+ lastDistanceAt = distanceAt;
+ lastCurve = curCurve.getCurvePoint()[curPoint];
+ curPoint++;
+
+ if (curPoint >= curCurve.getCurvesCount()) {
+ if (iter.hasNext()) {
+ curCurve = iter.next();
+ curPoint = 0;
+ } else {
+ curPoint = curCurve.getCurvesCount() - 1;
+ if (lastDistanceAt == distanceAt) {
+ // out of points even though the preferred distance hasn't been reached
+ break;
+ }
+ }
+ }
+ distanceAt += curCurve.getCurveDistances()[curPoint];
+ }
+ Vec2f thisCurve = curCurve.getCurvePoint()[curPoint];
+
+ // interpolate the point between the two closest distances
+ if (distanceAt - lastDistanceAt > 1) {
+ float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
+ curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
+ } else
+ curve[i] = thisCurve;
+ }
+
+// if (hitObject.getRepeatCount() > 1) {
+ Vec2f c1 = curve[0];
+ int cnt = 1;
+ Vec2f c2 = curve[cnt++];
+ while (cnt <= ncurve && c2.cpy().sub(c1).len() < 1)
+ c2 = curve[cnt++];
+ this.startAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
+
+ c1 = curve[ncurve];
+ cnt = ncurve - 1;
+ c2 = curve[cnt--];
+ while (cnt >= 0 && c2.cpy().sub(c1).len() < 1)
+ c2 = curve[cnt--];
+ this.endAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
+// }
+ }
+
+ @Override
+ public float[] pointAt(float t) {
+ float indexF = t * ncurve;
+ int index = (int) indexF;
+ if (index >= ncurve) {
+ Vec2f poi = curve[ncurve];
+ return new float[] { poi.x, poi.y };
+ } else {
+ Vec2f poi = curve[index];
+ Vec2f poi2 = curve[index + 1];
+ float t2 = indexF - index;
+ return new float[] {
+ lerp(poi.x, poi2.x, t2),
+ lerp(poi.y, poi2.y, t2)
+ };
+ }
+ }
+
+ @Override
+ public float getEndAngle() { return endAngle; }
+
+ @Override
+ public float getStartAngle() { return startAngle; }
+}
diff --git a/src/itdelatrisu/opsu/objects/curves/LinearBezier.java b/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
index 21e3b82c..3ec6717a 100644
--- a/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
+++ b/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
@@ -18,50 +18,46 @@
package itdelatrisu.opsu.objects.curves;
-import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.OsuHitObject;
-import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
-import java.util.Iterator;
import java.util.LinkedList;
import org.newdawn.slick.Color;
-import org.newdawn.slick.Image;
/**
- * Representation of a Bezier curve with equidistant points.
+ * Representation of Bezier curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
*
* @author fluddokt (https://github.com/fluddokt)
*/
-public class LinearBezier extends Curve {
- /** The angles of the first and last control points for drawing. */
- private float startAngle, endAngle;
-
- /** List of Bezier curves in the set of points. */
- private LinkedList beziers = new LinkedList();
-
- /** Points along the curve at equal distance. */
- private Vec2f[] curve;
-
- /** The number of points along the curve. */
- private int ncurve;
-
+public class LinearBezier extends EqualDistanceMultiCurve {
/**
* Constructor.
- * @param hitObject the associated OsuHitObject
+ * @param hitObject the associated HitObject
* @param color the color of this curve
+ * @param line whether a new curve should be generated for each sequential pair
*/
- public LinearBezier(OsuHitObject hitObject, Color color) {
+ public LinearBezier(HitObject hitObject, Color color, boolean line) {
super(hitObject, color);
- // splits points into different Beziers if has the same points (red points)
+ LinkedList beziers = new LinkedList();
+
+ // Beziers: splits points into different Beziers if has the same points (red points)
+ // a b c - c d - d e f g
+ // Lines: generate a new curve for each sequential pair
+ // ab bc cd de ef fg
int controlPoints = hitObject.getSliderX().length + 1;
LinkedList points = new LinkedList(); // temporary list of points to separate different Bezier curves
Vec2f lastPoi = null;
for (int i = 0; i < controlPoints; i++) {
Vec2f tpoi = new Vec2f(getX(i), getY(i));
- if (lastPoi != null && tpoi.equals(lastPoi)) {
+ if (line) {
+ if (lastPoi != null) {
+ points.add(tpoi);
+ beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
+ points.clear();
+ }
+ } else if (lastPoi != null && tpoi.equals(lastPoi)) {
if (points.size() >= 2)
beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
points.clear();
@@ -69,7 +65,7 @@ public class LinearBezier extends Curve {
points.add(tpoi);
lastPoi = tpoi;
}
- if (points.size() < 2) {
+ if (line || points.size() < 2) {
// trying to continue Bezier with less than 2 points
// probably ending on a red point, just ignore it
} else {
@@ -77,105 +73,6 @@ public class LinearBezier extends Curve {
points.clear();
}
- // find the length of all beziers
-// int totalDistance = 0;
-// for (Bezier2 bez : beziers) {
-// totalDistance += bez.totalDistance();
-// }
-
- // now try to creates points the are equidistant to each other
- this.ncurve = (int) (hitObject.getPixelLength() / 5f);
- this.curve = new Vec2f[ncurve + 1];
-
- float distanceAt = 0;
- Iterator iter = beziers.iterator();
- int curPoint = 0;
- Bezier2 curBezier = iter.next();
- Vec2f lastCurve = curBezier.getCurve()[0];
- float lastDistanceAt = 0;
-
- // length of Bezier should equal pixel length (in 640x480)
- float pixelLength = hitObject.getPixelLength() * OsuHitObject.getXMultiplier();
-
- // for each distance, try to get in between the two points that are between it
- for (int i = 0; i < ncurve + 1; i++) {
- int prefDistance = (int) (i * pixelLength / ncurve);
- while (distanceAt < prefDistance) {
- lastDistanceAt = distanceAt;
- lastCurve = curBezier.getCurve()[curPoint];
- distanceAt += curBezier.getCurveDistances()[curPoint++];
-
- if (curPoint >= curBezier.points()) {
- if (iter.hasNext()) {
- curBezier = iter.next();
- curPoint = 0;
- } else {
- curPoint = curBezier.points() - 1;
- if (lastDistanceAt == distanceAt) {
- // out of points even though the preferred distance hasn't been reached
- break;
- }
- }
- }
- }
- Vec2f thisCurve = curBezier.getCurve()[curPoint];
-
- // interpolate the point between the two closest distances
- if (distanceAt - lastDistanceAt > 1) {
- float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
- curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
- } else
- curve[i] = thisCurve;
- }
-
-// if (hitObject.getRepeatCount() > 1) {
- Vec2f c1 = curve[0];
- int cnt = 1;
- Vec2f c2 = curve[cnt++];
- while (cnt <= ncurve && c2.cpy().sub(c1).len() < 1)
- c2 = curve[cnt++];
- this.startAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
-
- c1 = curve[ncurve];
- cnt = ncurve - 1;
- c2 = curve[cnt--];
- while (cnt >= 0 && c2.cpy().sub(c1).len() < 1)
- c2 = curve[cnt--];
- this.endAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
-// }
+ init(beziers);
}
-
- @Override
- public float[] pointAt(float t) {
- float indexF = t * ncurve;
- int index = (int) indexF;
- if (index >= ncurve) {
- Vec2f poi = curve[ncurve];
- return new float[] { poi.x, poi.y };
- } else {
- Vec2f poi = curve[index];
- Vec2f poi2 = curve[index + 1];
- float t2 = indexF - index;
- return new float[] {
- lerp(poi.x, poi2.x, t2),
- lerp(poi.y, poi2.y, t2)
- };
- }
- }
-
- @Override
- public void draw() {
- Image hitCircle = GameImage.HITCIRCLE.getImage();
- Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
- for (int i = curve.length - 2; i >= 0; i--)
- hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
- for (int i = curve.length - 2; i >= 0; i--)
- hitCircle.drawCentered(curve[i].x, curve[i].y, color);
- }
-
- @Override
- public float getEndAngle() { return endAngle; }
-
- @Override
- public float getStartAngle() { return startAngle; }
}
diff --git a/src/itdelatrisu/opsu/objects/curves/Vec2f.java b/src/itdelatrisu/opsu/objects/curves/Vec2f.java
index b49bac2d..1005cd1d 100644
--- a/src/itdelatrisu/opsu/objects/curves/Vec2f.java
+++ b/src/itdelatrisu/opsu/objects/curves/Vec2f.java
@@ -49,6 +49,28 @@ public class Vec2f {
return new Vec2f((x + o.x) / 2, (y + o.y) / 2);
}
+ /**
+ * Scales the vector.
+ * @param s scaler to scale by
+ * @return itself (for chaining)
+ */
+ public Vec2f scale(float s) {
+ x *= s;
+ y *= s;
+ return this;
+ }
+
+ /**
+ * Adds a vector to this vector.
+ * @param o the other vector
+ * @return itself (for chaining)
+ */
+ public Vec2f add(Vec2f o) {
+ x += o.x;
+ y += o.y;
+ return this;
+ }
+
/**
* Subtracts a vector from this vector.
* @param o the other vector
@@ -97,4 +119,7 @@ public class Vec2f {
* @return true if the two vectors are numerically equal
*/
public boolean equals(Vec2f o) { return (x == o.x && y == o.y); }
+
+ @Override
+ public String toString() { return String.format("(%.3f, %.3f)", x, y); }
}
diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java
new file mode 100644
index 00000000..287ebd63
--- /dev/null
+++ b/src/itdelatrisu/opsu/render/CurveRenderState.java
@@ -0,0 +1,451 @@
+/*
+ * 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 .
+ */
+package itdelatrisu.opsu.render;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.curves.Vec2f;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+
+import org.lwjgl.BufferUtils;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL13;
+import org.lwjgl.opengl.GL14;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL20;
+import org.lwjgl.opengl.GL30;
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.util.Log;
+
+/**
+ * Hold the temporary render state that needs to be restored again after the new
+ * style curves are drawn.
+ *
+ * @author Bigpet {@literal }
+ */
+public class CurveRenderState {
+ /** The width and height of the display container this curve gets drawn into. */
+ protected static int containerWidth, containerHeight;
+
+ /** Thickness of the curve. */
+ protected static int scale;
+
+ /** Static state that's needed to draw the new style curves. */
+ private static final NewCurveStyleState staticState = new NewCurveStyleState();
+
+ /** Cached drawn slider, only used if new style sliders are activated. */
+ public Rendertarget fbo;
+
+ /** The HitObject associated with the curve to be drawn. */
+ protected HitObject hitObject;
+
+ /**
+ * Set the width and height of the container that Curves get drawn into.
+ * Should be called before any curves are drawn.
+ * @param width the container width
+ * @param height the container height
+ * @param circleSize the circle size
+ */
+ public static void init(int width, int height, float circleSize) {
+ containerWidth = width;
+ containerHeight = height;
+
+ // equivalent to what happens in Slider.init()
+ scale = (int) (104 - (circleSize * 8));
+ scale = (int) (scale * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
+ //scale = scale * 118 / 128; //for curves exactly as big as the sliderball
+ FrameBufferCache.init(width, height);
+ }
+
+ /**
+ * Creates an object to hold the render state that's necessary to draw a curve.
+ * @param hitObject the HitObject that represents this curve, just used as a unique ID
+ */
+ public CurveRenderState(HitObject hitObject) {
+ fbo = null;
+ this.hitObject = hitObject;
+ }
+
+ /**
+ * Draw a curve to the screen that's tinted with `color`. The first time
+ * this is called this caches the image result of the curve and on subsequent
+ * runs it just draws the cached copy to the screen.
+ * @param color tint of the curve
+ * @param borderColor the curve border color
+ * @param curve the points along the curve to be drawn
+ */
+ public void draw(Color color, Color borderColor, Vec2f[] curve) {
+ float alpha = color.a;
+
+ // if this curve hasn't been drawn, draw it and cache the result
+ if (fbo == null) {
+ FrameBufferCache cache = FrameBufferCache.getInstance();
+ Rendertarget mapping = cache.get(hitObject);
+ if (mapping == null)
+ mapping = cache.insert(hitObject);
+ fbo = mapping;
+
+ int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING);
+ int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
+
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
+ GL11.glViewport(0, 0, fbo.width, fbo.height);
+ GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+ Utils.COLOR_WHITE_FADE.a = 1.0f;
+ this.draw_curve(color, borderColor, curve);
+ color.a = 1f;
+
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex);
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb);
+ Utils.COLOR_WHITE_FADE.a = alpha;
+ }
+
+ // draw a fullscreen quad with the texture that contains the curve
+ GL11.glEnable(GL11.GL_TEXTURE_2D);
+ GL11.glDisable(GL11.GL_TEXTURE_1D);
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.getTextureID());
+ GL11.glBegin(GL11.GL_QUADS);
+ GL11.glColor4f(1.0f, 1.0f, 1.0f, alpha);
+ GL11.glTexCoord2f(1.0f, 1.0f);
+ GL11.glVertex2i(fbo.width, 0);
+ GL11.glTexCoord2f(0.0f, 1.0f);
+ GL11.glVertex2i(0, 0);
+ GL11.glTexCoord2f(0.0f, 0.0f);
+ GL11.glVertex2i(0, fbo.height);
+ GL11.glTexCoord2f(1.0f, 0.0f);
+ GL11.glVertex2i(fbo.width, fbo.height);
+ GL11.glEnd();
+ }
+
+ /**
+ * Discard the cache.
+ */
+ public void discardCache() {
+ fbo = null;
+ FrameBufferCache.getInstance().freeMappingFor(hitObject);
+ }
+
+ /**
+ * A structure to hold all the important OpenGL state that needs to be
+ * changed to draw the curve. This is used to backup and restore the state
+ * so that the code outside of this (mainly Slick2D) doesn't break.
+ */
+ private class RenderState {
+ boolean smoothedPoly;
+ boolean blendEnabled;
+ boolean depthEnabled;
+ boolean depthWriteEnabled;
+ boolean texEnabled;
+ int texUnit;
+ int oldProgram;
+ int oldArrayBuffer;
+ }
+
+ /**
+ * Backup the current state of the relevant OpenGL state and change it to
+ * what's needed to draw the curve.
+ */
+ private RenderState startRender() {
+ RenderState state = new RenderState();
+ state.smoothedPoly = GL11.glGetBoolean(GL11.GL_POLYGON_SMOOTH);
+ state.blendEnabled = GL11.glGetBoolean(GL11.GL_BLEND);
+ state.depthEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_TEST);
+ state.depthWriteEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_WRITEMASK);
+ state.texEnabled = GL11.glGetBoolean(GL11.GL_TEXTURE_2D);
+ state.texUnit = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE);
+ state.oldProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
+ state.oldArrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
+ GL11.glDisable(GL11.GL_POLYGON_SMOOTH);
+ GL11.glEnable(GL11.GL_BLEND);
+ GL14.glBlendEquation(GL14.GL_FUNC_ADD);
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ GL11.glEnable(GL11.GL_DEPTH_TEST);
+ GL11.glDepthMask(true);
+ GL11.glDisable(GL11.GL_TEXTURE_2D);
+ GL11.glEnable(GL11.GL_TEXTURE_1D);
+ GL11.glBindTexture(GL11.GL_TEXTURE_1D, staticState.gradientTexture);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
+
+ GL20.glUseProgram(0);
+
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ GL11.glPushMatrix();
+ GL11.glLoadIdentity();
+ GL11.glMatrixMode(GL11.GL_MODELVIEW);
+ GL11.glPushMatrix();
+ GL11.glLoadIdentity();
+
+ return state;
+ }
+
+ /**
+ * Restore the old OpenGL state that's backed up in {@code state}.
+ * @param state the old state to restore
+ */
+ private void endRender(RenderState state) {
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ GL11.glPopMatrix();
+ GL11.glMatrixMode(GL11.GL_MODELVIEW);
+ GL11.glPopMatrix();
+ GL11.glEnable(GL11.GL_BLEND);
+ GL20.glUseProgram(state.oldProgram);
+ GL13.glActiveTexture(state.texUnit);
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, state.oldArrayBuffer);
+ if (!state.depthWriteEnabled)
+ GL11.glDepthMask(false);
+ if (!state.depthEnabled)
+ GL11.glDisable(GL11.GL_DEPTH_TEST);
+ if (state.texEnabled)
+ GL11.glEnable(GL11.GL_TEXTURE_2D);
+ if (state.smoothedPoly)
+ GL11.glEnable(GL11.GL_POLYGON_SMOOTH);
+ if (!state.blendEnabled)
+ GL11.glDisable(GL11.GL_BLEND);
+ }
+
+ /**
+ * Do the actual drawing of the curve into the currently bound framebuffer.
+ * @param color the color of the curve
+ * @param borderColor the curve border color
+ * @param curve the points along the curve
+ */
+ private void draw_curve(Color color, Color borderColor, Vec2f[] curve) {
+ staticState.initGradient();
+ RenderState state = startRender();
+ int vtx_buf;
+ // the size is: floatsize * (position + texture coordinates) * (number of cones) * (vertices in a cone)
+ FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer();
+ staticState.initShaderProgram();
+ vtx_buf = GL15.glGenBuffers();
+ for (int i = 0; i < curve.length; ++i) {
+ float x = curve[i].x;
+ float y = curve[i].y;
+ //if (i == 0 || i == curve.length - 1){
+ fillCone(buff, x, y, NewCurveStyleState.DIVIDES);
+ if (i != 0) {
+ float last_x = curve[i - 1].x;
+ float last_y = curve[i - 1].y;
+ double diff_x = x - last_x;
+ double diff_y = y - last_y;
+ x = (float) (x - diff_x / 2);
+ y = (float) (y - diff_y / 2);
+ fillCone(buff, x, y, NewCurveStyleState.DIVIDES);
+ }
+ }
+ buff.flip();
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vtx_buf);
+ GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW);
+ GL20.glUseProgram(staticState.program);
+ GL20.glEnableVertexAttribArray(staticState.attribLoc);
+ GL20.glEnableVertexAttribArray(staticState.texCoordLoc);
+ GL20.glUniform1i(staticState.texLoc, 0);
+ GL20.glUniform3f(staticState.colLoc, color.r, color.g, color.b);
+ GL20.glUniform4f(staticState.colBorderLoc, borderColor.r, borderColor.g, borderColor.b, borderColor.a);
+ //stride is 6*4 for the floats (4 bytes) (u,v)(x,y,z,w)
+ //2*4 is for skipping the first 2 floats (u,v)
+ GL20.glVertexAttribPointer(staticState.attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4);
+ GL20.glVertexAttribPointer(staticState.texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0);
+ for (int i = 0; i < curve.length * 2 - 1; ++i)
+ GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2);
+ GL20.glDisableVertexAttribArray(staticState.texCoordLoc);
+ GL20.glDisableVertexAttribArray(staticState.attribLoc);
+ GL15.glDeleteBuffers(vtx_buf);
+ endRender(state);
+ }
+
+ /**
+ * Fill {@code buff} with the texture coordinates and positions for a cone
+ * with {@code DIVIDES} ground corners that has its center at the coordinates
+ * {@code (x1,y1)}.
+ * @param buff the buffer to be filled
+ * @param x1 x-coordinate of the cone
+ * @param y1 y-coordinate of the cone
+ * @param DIVIDES the base of the cone is a regular polygon with this many sides
+ */
+ protected void fillCone(FloatBuffer buff, float x1, float y1, final int DIVIDES) {
+ float divx = containerWidth / 2.0f;
+ float divy = containerHeight / 2.0f;
+ float offx = -1.0f;
+ float offy = 1.0f;
+ float x, y;
+ float radius = scale / 2;
+ buff.put(1.0f);
+ buff.put(0.5f);
+ //GL11.glTexCoord2d(1.0, 0.5);
+ x = offx + x1 / divx;
+ y = offy - y1 / divy;
+ buff.put(x);
+ buff.put(y);
+ buff.put(0f);
+ buff.put(1f);
+ //GL11.glVertex4f(x, y, 0.0f, 1.0f);
+ for (int j = 0; j < DIVIDES; ++j) {
+ double phase = j * (float) Math.PI * 2 / DIVIDES;
+ buff.put(0.0f);
+ buff.put(0.5f);
+ //GL11.glTexCoord2d(0.0, 0.5);
+ x = (x1 + radius * (float) Math.sin(phase)) / divx;
+ y = (y1 + radius * (float) Math.cos(phase)) / divy;
+ buff.put((offx + x));
+ buff.put((offy - y));
+ buff.put(1f);
+ buff.put(1f);
+ //GL11.glVertex4f(x + 90 * (float) Math.sin(phase), y + 90 * (float) Math.cos(phase), 1.0f, 1.0f);
+ }
+ buff.put(0.0f);
+ buff.put(0.5f);
+ //GL11.glTexCoord2d(0.0, 0.5);
+ x = (x1 + radius * (float) Math.sin(0.0)) / divx;
+ y = (y1 + radius * (float) Math.cos(0.0)) / divy;
+ buff.put((offx + x));
+ buff.put((offy - y));
+ buff.put(1f);
+ buff.put(1f);
+ //GL11.glVertex4f(x + 90 * (float) Math.sin(0.0), y + 90 * (float) Math.cos(0.0), 1.0f, 1.0f);
+ }
+
+ /**
+ * Contains all the necessary state that needs to be tracked to draw curves
+ * in the new style and not re-create the shader each time.
+ *
+ * @author Bigpet {@literal }
+ */
+ private static class NewCurveStyleState {
+ /**
+ * Used for new style Slider rendering, defines how many vertices the
+ * base of the cone has that is used to draw the curve.
+ */
+ protected static final int DIVIDES = 30;
+
+ /** OpenGL shader program ID used to draw and recolor the curve. */
+ protected int program = 0;
+
+ /** OpenGL shader attribute location of the vertex position attribute. */
+ protected int attribLoc = 0;
+
+ /** OpenGL shader attribute location of the texture coordinate attribute. */
+ protected int texCoordLoc = 0;
+
+ /** OpenGL shader uniform location of the color attribute. */
+ protected int colLoc = 0;
+
+ /** OpenGL shader uniform location of the border color attribute. */
+ protected int colBorderLoc = 0;
+
+ /** OpenGL shader uniform location of the texture sampler attribute. */
+ protected int texLoc = 0;
+
+ /** OpenGL texture id for the gradient texture for the curve. */
+ protected int gradientTexture = 0;
+
+ /**
+ * Reads the first row of the slider gradient texture and upload it as
+ * a 1D texture to OpenGL if it hasn't already been done.
+ */
+ public void initGradient() {
+ if (gradientTexture == 0) {
+ Image slider = GameImage.SLIDER_GRADIENT.getImage().getScaledCopy(1.0f / GameImage.getUIscale());
+ staticState.gradientTexture = GL11.glGenTextures();
+ ByteBuffer buff = BufferUtils.createByteBuffer(slider.getWidth() * 4);
+ for (int i = 0; i < slider.getWidth(); ++i) {
+ Color col = slider.getColor(i, 0);
+ buff.put((byte) (255 * col.r));
+ buff.put((byte) (255 * col.g));
+ buff.put((byte) (255 * col.b));
+ buff.put((byte) (255 * col.a));
+ }
+ buff.flip();
+ GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
+ GL11.glTexImage1D(GL11.GL_TEXTURE_1D, 0, GL11.GL_RGBA, slider.getWidth(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buff);
+ GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D);
+ }
+ }
+
+ /**
+ * Compiles and links the shader program for the new style curve objects
+ * if it hasn't already been compiled and linked.
+ */
+ public void initShaderProgram() {
+ if (program == 0) {
+ program = GL20.glCreateProgram();
+ int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
+ int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
+ GL20.glShaderSource(vtxShdr, "#version 330\n"
+ + "\n"
+ + "layout(location = 0) in vec4 in_position;\n"
+ + "layout(location = 1) in vec2 in_tex_coord;\n"
+ + "\n"
+ + "out vec2 tex_coord;\n"
+ + "void main()\n"
+ + "{\n"
+ + " gl_Position = in_position;\n"
+ + " tex_coord = in_tex_coord;\n"
+ + "}");
+ GL20.glCompileShader(vtxShdr);
+ int res = GL20.glGetShaderi(vtxShdr, GL20.GL_COMPILE_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
+ Log.error("Vertex Shader compilation failed.", new Exception(error));
+ }
+ GL20.glShaderSource(frgShdr, "#version 330\n"
+ + "\n"
+ + "uniform sampler1D tex;\n"
+ + "uniform vec2 tex_size;\n"
+ + "uniform vec3 col_tint;\n"
+ + "uniform vec4 col_border;\n"
+ + "\n"
+ + "in vec2 tex_coord;\n"
+ + "layout(location = 0) out vec4 out_colour;\n"
+ + "\n"
+ + "void main()\n"
+ + "{\n"
+ + " vec4 in_color = texture(tex, tex_coord.x);\n"
+ + " float blend_factor = in_color.r-in_color.b;\n"
+ + " vec4 new_color = vec4(mix(in_color.xyz*col_border.xyz,col_tint,blend_factor),in_color.w);\n"
+ + " out_colour = new_color;\n"
+ + "}");
+ GL20.glCompileShader(frgShdr);
+ res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetShaderInfoLog(frgShdr, 1024);
+ Log.error("Fragment Shader compilation failed.", new Exception(error));
+ }
+ GL20.glAttachShader(program, vtxShdr);
+ GL20.glAttachShader(program, frgShdr);
+ GL20.glLinkProgram(program);
+ res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetProgramInfoLog(program, 1024);
+ Log.error("Program linking failed.", new Exception(error));
+ }
+ attribLoc = GL20.glGetAttribLocation(program, "in_position");
+ texCoordLoc = GL20.glGetAttribLocation(program, "in_tex_coord");
+ texLoc = GL20.glGetUniformLocation(program, "tex");
+ colLoc = GL20.glGetUniformLocation(program, "col_tint");
+ colBorderLoc = GL20.glGetUniformLocation(program, "col_border");
+ }
+ }
+ }
+}
diff --git a/src/itdelatrisu/opsu/render/FrameBufferCache.java b/src/itdelatrisu/opsu/render/FrameBufferCache.java
new file mode 100644
index 00000000..2558dd15
--- /dev/null
+++ b/src/itdelatrisu/opsu/render/FrameBufferCache.java
@@ -0,0 +1,136 @@
+/*
+ * 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 .
+ */
+package itdelatrisu.opsu.render;
+
+import itdelatrisu.opsu.beatmap.HitObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This is cache for OpenGL FrameBufferObjects. This is currently only used
+ * to draw curve objects of the new slider style. Does currently not integrate
+ * well and requires some manual OpenGL state manipulation to use it.
+ *
+ * @author Bigpet {@literal }
+ */
+public class FrameBufferCache {
+ /** The single framebuffer cache instance. */
+ private static FrameBufferCache instance = null;
+
+ /** The mapping from hit objects to framebuffers. */
+ private Map cacheMap;
+
+ /** */
+ private ArrayList cache;
+
+ /** Container dimensions. */
+ public static int width, height;
+
+ /**
+ * Set the width and height of the framebuffers in this cache.
+ * Should be called before anything is inserted into the map.
+ * @param width the container width
+ * @param height the container height
+ */
+ public static void init(int width, int height) {
+ FrameBufferCache.width = width;
+ FrameBufferCache.height = height;
+ }
+
+ /**
+ * Constructor.
+ */
+ private FrameBufferCache() {
+ cache = new ArrayList();
+ cacheMap = new HashMap();
+ }
+
+ /**
+ * Check if there is a framebuffer object mapped to {@code obj}.
+ * @param obj the hit object
+ * @return true if there is a framebuffer mapped for this {@code HitObject}, else false
+ */
+ public boolean contains(HitObject obj) {
+ return cacheMap.containsKey(obj);
+ }
+
+ /**
+ * Get the {@code Rendertarget} mapped to {@code obj}.
+ * @param obj the hit object
+ * @return the {@code Rendertarget} if there's one mapped to {@code obj}, otherwise null
+ */
+ public Rendertarget get(HitObject obj) {
+ return cacheMap.get(obj);
+ }
+
+ /**
+ * Clear the mapping for {@code obj} to free it up to get used by another {@code HitObject}.
+ * @param obj the hit object
+ * @return true if there was a mapping for {@code obj} and false if there was no mapping for it.
+ */
+ public boolean freeMappingFor(HitObject obj) {
+ return cacheMap.remove(obj) != null;
+ }
+
+ /**
+ * Clear the cache of all the mappings. This does not actually delete the
+ * cached framebuffers, it merely frees them all up to get mapped anew.
+ */
+ public void freeMap() {
+ cacheMap.clear();
+ }
+
+ /**
+ * Create a mapping from {@code obj} to a framebuffer. If there was already
+ * a mapping from {@code obj} this will associate another framebuffer with it
+ * (thereby freeing up the previously mapped framebuffer).
+ * @param obj the hit object
+ * @return the {@code Rendertarget} newly mapped to {@code obj}
+ */
+ public Rendertarget insert(HitObject obj) {
+ // find first RTTFramebuffer that's not mapped to anything and return it
+ Rendertarget buffer;
+ for (int i = 0; i < cache.size(); ++i) {
+ buffer = cache.get(i);
+ if (!cacheMap.containsValue(buffer)) {
+ cacheMap.put(obj, buffer);
+ return buffer;
+ }
+ }
+
+ // no unmapped RTTFramebuffer found, create a new one
+ buffer = Rendertarget.createRTTFramebuffer(width, height);
+ cache.add(buffer);
+ cacheMap.put(obj, buffer);
+ return buffer;
+ }
+
+ /**
+ * There should only ever be one framebuffer cache, this function returns
+ * that one framebuffer cache instance.
+ * If there was no instance created already then this function creates it.
+ * @return the instance of FrameBufferCache
+ */
+ public static FrameBufferCache getInstance() {
+ if (instance == null)
+ instance = new FrameBufferCache();
+ return instance;
+ }
+}
diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java
new file mode 100644
index 00000000..00599f5f
--- /dev/null
+++ b/src/itdelatrisu/opsu/render/Rendertarget.java
@@ -0,0 +1,115 @@
+/*
+ * 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 .
+ */
+package itdelatrisu.opsu.render;
+
+import java.nio.ByteBuffer;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL20;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL32;
+
+/**
+ * Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL.
+ *
+ * @author Bigpet {@literal }
+ */
+public class Rendertarget {
+ /** The dimensions. */
+ public final int width, height;
+
+ /** The FBO ID. */
+ private final int fboID;
+
+ /** The texture ID. */
+ private final int textureID;
+
+ /**
+ * Create a new FBO.
+ * @param width the width
+ * @param height the height
+ */
+ private Rendertarget(int width, int height) {
+ this.width = width;
+ this.height = height;
+ fboID = GL30.glGenFramebuffers();
+ textureID = GL11.glGenTextures();
+ }
+
+ /**
+ * Bind this rendertarget as the primary framebuffer.
+ */
+ public void bind() {
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID);
+ }
+
+ /**
+ * Returns the FBO ID.
+ */
+ // NOTE: use judiciously, try to avoid if possible and consider adding a
+ // method to this class if you find yourself calling this repeatedly.
+ public int getID() {
+ return fboID;
+ }
+
+ /**
+ * Returns the texture ID.
+ */
+ // NOTE: try not to use, could be moved into separate class.
+ public int getTextureID() {
+ return textureID;
+ }
+
+ /**
+ * Bind the default framebuffer.
+ */
+ public static void unbind() {
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0);
+ }
+
+ /**
+ * Creates a Rendertarget with a Texture that it renders the color buffer in
+ * and a renderbuffer that it renders the depth to.
+ * @param width the width
+ * @param height the height
+ */
+ public static Rendertarget createRTTFramebuffer(int width, int height) {
+ int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING);
+ int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
+ Rendertarget buffer = new Rendertarget(width,height);
+ buffer.bind();
+
+ int fboTexture = buffer.textureID;
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, fboTexture);
+ GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, 4, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_INT, (ByteBuffer) null);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
+
+ int fboDepth = GL30.glGenRenderbuffers();
+ GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, fboDepth);
+ GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
+ GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, fboDepth);
+
+ GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0);
+ GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
+
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer);
+
+ return buffer;
+ }
+}
diff --git a/src/itdelatrisu/opsu/replay/LifeFrame.java b/src/itdelatrisu/opsu/replay/LifeFrame.java
index f53e8745..045075f9 100644
--- a/src/itdelatrisu/opsu/replay/LifeFrame.java
+++ b/src/itdelatrisu/opsu/replay/LifeFrame.java
@@ -16,13 +16,13 @@
* along with opsu!. If not, see .
*/
+package itdelatrisu.opsu.replay;
+
/**
* Captures a single life frame.
*
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
*/
-package itdelatrisu.opsu.replay;
-
public class LifeFrame {
/** Time. */
private int time;
diff --git a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java
new file mode 100644
index 00000000..ec2fad6d
--- /dev/null
+++ b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java
@@ -0,0 +1,93 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.replay;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.GameMod;
+import itdelatrisu.opsu.ui.MenuButton;
+
+import org.newdawn.slick.Image;
+
+/**
+ * Playback speeds for replays.
+ *
+ * @author DarkTigrus (https://github.com/DarkTigrus)
+ */
+public enum PlaybackSpeed {
+ NORMAL (GameImage.REPLAY_PLAYBACK_NORMAL, 1f),
+ DOUBLE (GameImage.REPLAY_PLAYBACK_DOUBLE, 2f),
+ HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
+
+ /** The button image. */
+ private GameImage gameImage;
+
+ /** The button. */
+ private MenuButton button;
+
+ /** The playback speed modifier. */
+ private float modifier;
+
+ /** Enum values. */
+ private static PlaybackSpeed[] values = PlaybackSpeed.values();
+
+ /**
+ * Initializes the playback buttons.
+ * @param width the container width
+ * @param height the container height
+ */
+ public static void init(int width, int height) {
+ for (PlaybackSpeed playback : PlaybackSpeed.values()) {
+ Image img = playback.gameImage.getImage();
+ playback.button = new MenuButton(img, width * 0.98f - (img.getWidth() / 2f), height * 0.25f);
+ playback.button.setHoverFade();
+ }
+ }
+
+ /**
+ * Constructor.
+ * @param gameImage the button image
+ * @param modifier the speed modifier
+ */
+ PlaybackSpeed(GameImage gameImage, float modifier) {
+ this.gameImage = gameImage;
+ this.modifier = modifier;
+ }
+
+ /**
+ * Returns the button.
+ * @return the associated button
+ */
+ public MenuButton getButton() { return button; }
+
+ /**
+ * Returns the speed modifier.
+ * @return the speed
+ */
+ public float getModifier() { return modifier; }
+
+ /**
+ * Returns the next playback speed.
+ */
+ public PlaybackSpeed next() {
+ PlaybackSpeed next = values[(this.ordinal() + 1) % values.length];
+ if ((GameMod.DOUBLE_TIME.isActive() && next == PlaybackSpeed.DOUBLE))
+ next = next.next();
+ return next;
+ }
+}
diff --git a/src/itdelatrisu/opsu/replay/ReplayFrame.java b/src/itdelatrisu/opsu/replay/ReplayFrame.java
index 393f1494..9f8e9f98 100644
--- a/src/itdelatrisu/opsu/replay/ReplayFrame.java
+++ b/src/itdelatrisu/opsu/replay/ReplayFrame.java
@@ -18,7 +18,7 @@
package itdelatrisu.opsu.replay;
-import itdelatrisu.opsu.OsuHitObject;
+import itdelatrisu.opsu.beatmap.HitObject;
/**
* Captures a single replay frame.
@@ -98,12 +98,12 @@ public class ReplayFrame {
/**
* Returns the scaled cursor x coordinate.
*/
- public int getScaledX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
+ public int getScaledX() { return (int) (x * HitObject.getXMultiplier() + HitObject.getXOffset()); }
/**
* Returns the scaled cursor y coordinate.
*/
- public int getScaledY() { return (int) (y * OsuHitObject.getYMultiplier() + OsuHitObject.getYOffset()); }
+ public int getScaledY() { return (int) (y * HitObject.getYMultiplier() + HitObject.getYOffset()); }
/**
* Returns the keys pressed (KEY_* bitmask).
diff --git a/src/itdelatrisu/opsu/skins/Skin.java b/src/itdelatrisu/opsu/skins/Skin.java
new file mode 100644
index 00000000..28c40b0b
--- /dev/null
+++ b/src/itdelatrisu/opsu/skins/Skin.java
@@ -0,0 +1,374 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.skins;
+
+import java.io.File;
+
+import org.newdawn.slick.Color;
+
+/**
+ * Skin configuration (skin.ini).
+ */
+public class Skin {
+ /** The default skin name. */
+ public static final String DEFAULT_SKIN_NAME = "Default";
+
+ /** Slider styles. */
+ public static final byte
+ STYLE_PEPPYSLIDER = 1, // fallback
+ STYLE_MMSLIDER = 2, // default (requires OpenGL 3.0)
+ STYLE_TOONSLIDER = 3, // not implemented
+ STYLE_OPENGLSLIDER = 4; // not implemented
+
+ /** The latest skin version. */
+ protected static final int LATEST_VERSION = 2;
+
+ /** The default list of combos with combo sounds. */
+ private static final int[] DEFAULT_CUSTOM_COMBO_BURST_SOUNDS = { 50, 75, 100, 200, 300 };
+
+ /** The default combo colors (used when a beatmap does not provide custom colors). */
+ private static final Color[] DEFAULT_COMBO = {
+ new Color(255, 192, 0),
+ new Color(0, 202, 0),
+ new Color(18, 124, 255),
+ new Color(242, 24, 57)
+ };
+
+ /** The default menu visualization bar color. */
+ private static final Color DEFAULT_MENU_GLOW = new Color(0, 78, 155);
+
+ /** The default slider border color. */
+ private static final Color DEFAULT_SLIDER_BORDER = new Color(255, 255, 255);
+
+ /** The default slider ball color. */
+ private static final Color DEFAULT_SLIDER_BALL = new Color(2, 170, 255);
+
+ /** The default spinner approach circle color. */
+ private static final Color DEFAULT_SPINNER_APPROACH_CIRCLE = new Color(77, 139, 217);
+
+ /** The default color of the active text in the song selection menu. */
+ private static final Color DEFAULT_SONG_SELECT_ACTIVE_TEXT = new Color(255, 255, 255);
+
+ /** The default color of the inactive text in the song selection menu. */
+ private static final Color DEFAULT_SONG_SELECT_INACTIVE_TEXT = new Color(178, 178, 178);
+
+ /** The default color of the stars that fall from the cursor during breaks. */
+ private static final Color DEFAULT_STAR_BREAK_ADDITIVE = new Color(255, 182, 193);
+
+ /** The skin directory. */
+ private File dir;
+
+ /**
+ * [General]
+ */
+
+ /** The name of the skin. */
+ protected String name = "opsu! Default Skin";
+
+ /** The skin author. */
+ protected String author = "[various authors]";
+
+ /** The skin version. */
+ protected int version = LATEST_VERSION;
+
+ /** When a slider has a reverse, should the ball sprite flip horizontally? */
+ protected boolean sliderBallFlip = false;
+
+ /** Should the cursor sprite rotate constantly? */
+ protected boolean cursorRotate = true;
+
+ /** Should the cursor expand when clicked? */
+ protected boolean cursorExpand = true;
+
+ /** Should the cursor have an origin at the center of the image? (if not, the top-left corner is used) */
+ protected boolean cursorCentre = true;
+
+ /** The number of frames in the slider ball animation. */
+ protected int sliderBallFrames = 10;
+
+ /** Should the hitcircleoverlay sprite be drawn above the hircircle combo number? */
+ protected boolean hitCircleOverlayAboveNumber = true;
+
+ /** Should the sound frequency be modulated depending on the spinner score? */
+ protected boolean spinnerFrequencyModulate = false;
+
+ /** Should the normal hitsound always be played? */
+ protected boolean layeredHitSounds = true;
+
+ /** Should the spinner fade the playfield? */
+ protected boolean spinnerFadePlayfield = true;
+
+ /** Should the last spinner bar blink? */
+ protected boolean spinnerNoBlink = false;
+
+ /** Should the slider combo color tint the slider ball? */
+ protected boolean allowSliderBallTint = false;
+
+ /** The FPS of animations. */
+ protected int animationFramerate = -1;
+
+ /** Should the cursor trail sprite rotate constantly? */
+ protected boolean cursorTrailRotate = false;
+
+ /** List of combos with combo sounds. */
+ protected int[] customComboBurstSounds = DEFAULT_CUSTOM_COMBO_BURST_SOUNDS;
+
+ /** Should the combo burst sprites appear in random order? */
+ protected boolean comboBurstRandom = false;
+
+ /** The slider style to use (see STYLE_* constants). */
+ protected byte sliderStyle = STYLE_MMSLIDER;
+
+ /**
+ * [Colours]
+ */
+
+ /** Combo colors (max 8). */
+ protected Color[] combo = DEFAULT_COMBO;
+
+ /** The menu visualization bar color. */
+ protected Color menuGlow = DEFAULT_MENU_GLOW;
+
+ /** The color for the slider border. */
+ protected Color sliderBorder = DEFAULT_SLIDER_BORDER;
+
+ /** The slider ball color. */
+ protected Color sliderBall = DEFAULT_SLIDER_BALL;
+
+ /** The spinner approach circle color. */
+ protected Color spinnerApproachCircle = DEFAULT_SPINNER_APPROACH_CIRCLE;
+
+ /** The color of text in the currently active group in song selection. */
+ protected Color songSelectActiveText = DEFAULT_SONG_SELECT_ACTIVE_TEXT;
+
+ /** The color of text in the inactive groups in song selection. */
+ protected Color songSelectInactiveText = DEFAULT_SONG_SELECT_INACTIVE_TEXT;
+
+ /** The color of the stars that fall from the cursor (star2 sprite) in breaks. */
+ protected Color starBreakAdditive = DEFAULT_STAR_BREAK_ADDITIVE;
+
+ /**
+ * [Fonts]
+ */
+
+ /** The prefix for the hitcircle font sprites. */
+ protected String hitCirclePrefix = "default";
+
+ /** How much should the hitcircle font sprites overlap? */
+ protected int hitCircleOverlap = -2;
+
+ /** The prefix for the score font sprites. */
+ protected String scorePrefix = "score";
+
+ /** How much should the score font sprites overlap? */
+ protected int scoreOverlap = 0;
+
+ /** The prefix for the combo font sprites. */
+ protected String comboPrefix = "score";
+
+ /** How much should the combo font sprites overlap? */
+ protected int comboOverlap = 0;
+
+ /**
+ * Constructor.
+ * @param dir the skin directory
+ */
+ public Skin(File dir) {
+ this.dir = dir;
+ }
+
+ /**
+ * Returns the skin directory.
+ */
+ public File getDirectory() { return dir; }
+
+ /**
+ * Returns the name of the skin.
+ */
+ public String getName() { return name; }
+
+ /**
+ * Returns the skin author.
+ */
+ public String getAuthor() { return author; }
+
+ /**
+ * Returns the skin version.
+ */
+ public int getVersion() { return version; }
+
+ /**
+ * Returns whether the slider ball should be flipped horizontally during a reverse.
+ */
+ public boolean isSliderBallFlipped() { return sliderBallFlip; }
+
+ /**
+ * Returns whether the cursor should rotate.
+ */
+ public boolean isCursorRotated() { return cursorRotate; }
+
+ /**
+ * Returns whether the cursor should expand when clicked.
+ */
+ public boolean isCursorExpanded() { return cursorExpand; }
+
+ /**
+ * Returns whether the cursor should have an origin in the center.
+ * @return {@code true} if center, {@code false} if top-left corner
+ */
+ public boolean isCursorCentered() { return cursorCentre; }
+
+ /**
+ * Returns the number of frames in the slider ball animation.
+ */
+ public int getSliderBallFrames() { return sliderBallFrames; }
+
+ /**
+ * Returns whether the hit circle overlay should be drawn above the combo number.
+ */
+ public boolean isHitCircleOverlayAboveNumber() { return hitCircleOverlayAboveNumber; }
+
+ /**
+ * Returns whether the sound frequency should be modulated depending on the spinner score.
+ */
+ public boolean isSpinnerFrequencyModulated() { return spinnerFrequencyModulate; }
+
+ /**
+ * Returns whether the normal hitsound should always be played (and layered on other sounds).
+ */
+ public boolean isLayeredHitSounds() { return layeredHitSounds; }
+
+ /**
+ * Returns whether the playfield should fade for spinners.
+ */
+ public boolean isSpinnerFadePlayfield() { return spinnerFadePlayfield; }
+
+ /**
+ * Returns whether the last spinner bar should blink.
+ */
+ public boolean isSpinnerNoBlink() { return spinnerNoBlink; }
+
+ /**
+ * Returns whether the slider ball should be tinted with the slider combo color.
+ */
+ public boolean isAllowSliderBallTint() { return allowSliderBallTint; }
+
+ /**
+ * Returns the frame rate of animations.
+ * @return the FPS, or {@code -1} (TODO)
+ */
+ public int getAnimationFramerate() { return animationFramerate; }
+
+ /**
+ * Returns whether the cursor trail should rotate.
+ */
+ public boolean isCursorTrailRotated() { return cursorTrailRotate; }
+
+ /**
+ * Returns a list of combos with combo sounds.
+ */
+ public int[] getCustomComboBurstSounds() { return customComboBurstSounds; }
+
+ /**
+ * Returns whether combo bursts should appear in random order.
+ */
+ public boolean isComboBurstRandom() { return comboBurstRandom; }
+
+ /**
+ * Returns the slider style.
+ *
+ * - 1: peppysliders (segmented)
+ *
- 2: mmsliders (smooth)
+ *
- 3: toonsliders (smooth, with steps instead of gradient)
+ *
- 4: legacy OpenGL-only sliders
+ *
+ * @return the style (see STYLE_* constants)
+ */
+ public byte getSliderStyle() { return sliderStyle; }
+
+ /**
+ * Returns the list of combo colors (max 8).
+ */
+ public Color[] getComboColors() { return combo; }
+
+ /**
+ * Returns the menu visualization bar color.
+ */
+ public Color getMenuGlowColor() { return menuGlow; }
+
+ /**
+ * Returns the slider border color.
+ */
+ public Color getSliderBorderColor() { return sliderBorder; }
+
+ /**
+ * Returns the slider ball color.
+ */
+ public Color getSliderBallColor() { return sliderBall; }
+
+ /**
+ * Returns the spinner approach circle color.
+ */
+ public Color getSpinnerApproachCircleColor() { return spinnerApproachCircle; }
+
+ /**
+ * Returns the color of the active text in the song selection menu.
+ */
+ public Color getSongSelectActiveTextColor() { return songSelectActiveText; }
+
+ /**
+ * Returns the color of the inactive text in the song selection menu.
+ */
+ public Color getSongSelectInactiveTextColor() { return songSelectInactiveText; }
+
+ /**
+ * Returns the color of the stars that fall from the cursor during breaks.
+ */
+ public Color getStarBreakAdditiveColor() { return starBreakAdditive; }
+
+ /**
+ * Returns the prefix for the hit circle font sprites.
+ */
+ public String getHitCircleFontPrefix() { return hitCirclePrefix; }
+
+ /**
+ * Returns the amount of overlap between the hit circle font sprites.
+ */
+ public int getHitCircleFontOverlap() { return hitCircleOverlap; }
+
+ /**
+ * Returns the prefix for the score font sprites.
+ */
+ public String getScoreFontPrefix() { return scorePrefix; }
+
+ /**
+ * Returns the amount of overlap between the score font sprites.
+ */
+ public int getScoreFontOverlap() { return scoreOverlap; }
+
+ /**
+ * Returns the prefix for the combo font sprites.
+ */
+ public String getComboFontPrefix() { return comboPrefix; }
+
+ /**
+ * Returns the amount of overlap between the combo font sprites.
+ */
+ public int getComboFontOverlap() { return comboOverlap; }
+}
diff --git a/src/itdelatrisu/opsu/skins/SkinLoader.java b/src/itdelatrisu/opsu/skins/SkinLoader.java
new file mode 100644
index 00000000..a0a7362e
--- /dev/null
+++ b/src/itdelatrisu/opsu/skins/SkinLoader.java
@@ -0,0 +1,299 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.skins;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.Utils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+import org.newdawn.slick.Color;
+import org.newdawn.slick.util.Log;
+
+/**
+ * Loads skin configuration files.
+ */
+public class SkinLoader {
+ /** Name of the skin configuration file. */
+ private static final String CONFIG_FILENAME = "skin.ini";
+
+ // This class should not be instantiated.
+ private SkinLoader() {}
+
+ /**
+ * Returns a list of all subdirectories in the Skins directory.
+ * @param root the root directory (search has depth 1)
+ * @return an array of skin directories
+ */
+ public static File[] getSkinDirectories(File root) {
+ ArrayList dirs = new ArrayList();
+ for (File dir : root.listFiles()) {
+ if (dir.isDirectory())
+ dirs.add(dir);
+ }
+ return dirs.toArray(new File[dirs.size()]);
+ }
+
+ /**
+ * Loads a skin configuration file.
+ * If 'skin.ini' is not found, or if any fields are not specified, the
+ * default values will be used.
+ * @param dir the skin directory
+ * @return the loaded skin
+ */
+ public static Skin loadSkin(File dir) {
+ File skinFile = new File(dir, CONFIG_FILENAME);
+ Skin skin = new Skin(dir);
+ if (!skinFile.isFile()) // missing skin.ini
+ return skin;
+
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(skinFile), "UTF-8"))) {
+ String line = in.readLine();
+ String tokens[] = null;
+ while (line != null) {
+ line = line.trim();
+ if (!isValidLine(line)) {
+ line = in.readLine();
+ continue;
+ }
+ switch (line) {
+ case "[General]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ switch (tokens[0]) {
+ case "Name":
+ skin.name = tokens[1];
+ break;
+ case "Author":
+ skin.author = tokens[1];
+ break;
+ case "Version":
+ if (tokens[1].equalsIgnoreCase("latest"))
+ skin.version = Skin.LATEST_VERSION;
+ else
+ skin.version = Integer.parseInt(tokens[1]);
+ break;
+ case "SliderBallFlip":
+ skin.sliderBallFlip = Utils.parseBoolean(tokens[1]);
+ break;
+ case "CursorRotate":
+ skin.cursorRotate = Utils.parseBoolean(tokens[1]);
+ break;
+ case "CursorExpand":
+ skin.cursorExpand = Utils.parseBoolean(tokens[1]);
+ break;
+ case "CursorCentre":
+ skin.cursorCentre = Utils.parseBoolean(tokens[1]);
+ break;
+ case "SliderBallFrames":
+ skin.sliderBallFrames = Integer.parseInt(tokens[1]);
+ break;
+ case "HitCircleOverlayAboveNumber":
+ skin.hitCircleOverlayAboveNumber = Utils.parseBoolean(tokens[1]);
+ break;
+ case "spinnerFrequencyModulate":
+ skin.spinnerFrequencyModulate = Utils.parseBoolean(tokens[1]);
+ break;
+ case "LayeredHitSounds":
+ skin.layeredHitSounds = Utils.parseBoolean(tokens[1]);
+ break;
+ case "SpinnerFadePlayfield":
+ skin.spinnerFadePlayfield = Utils.parseBoolean(tokens[1]);
+ break;
+ case "SpinnerNoBlink":
+ skin.spinnerNoBlink = Utils.parseBoolean(tokens[1]);
+ break;
+ case "AllowSliderBallTint":
+ skin.allowSliderBallTint = Utils.parseBoolean(tokens[1]);
+ break;
+ case "AnimationFramerate":
+ skin.animationFramerate = Integer.parseInt(tokens[1]);
+ break;
+ case "CursorTrailRotate":
+ skin.cursorTrailRotate = Utils.parseBoolean(tokens[1]);
+ break;
+ case "CustomComboBurstSounds":
+ String[] split = tokens[1].split(",");
+ int[] customComboBurstSounds = new int[split.length];
+ for (int i = 0; i < split.length; i++)
+ customComboBurstSounds[i] = Integer.parseInt(split[i]);
+ skin.customComboBurstSounds = customComboBurstSounds;
+ break;
+ case "ComboBurstRandom":
+ skin.comboBurstRandom = Utils.parseBoolean(tokens[1]);
+ break;
+ case "SliderStyle":
+ skin.sliderStyle = Byte.parseByte(tokens[1]);
+ break;
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read line '%s' for file '%s'.",
+ line, skinFile.getAbsolutePath()), e);
+ }
+ }
+ break;
+ case "[Colours]":
+ LinkedList colors = new LinkedList();
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ String[] rgb = tokens[1].split(",");
+ Color color = new Color(
+ Integer.parseInt(rgb[0]),
+ Integer.parseInt(rgb[1]),
+ Integer.parseInt(rgb[2])
+ );
+ switch (tokens[0]) {
+ case "Combo1":
+ case "Combo2":
+ case "Combo3":
+ case "Combo4":
+ case "Combo5":
+ case "Combo6":
+ case "Combo7":
+ case "Combo8":
+ colors.add(color);
+ break;
+ case "MenuGlow":
+ skin.menuGlow = color;
+ break;
+ case "SliderBorder":
+ skin.sliderBorder = color;
+ break;
+ case "SliderBall":
+ skin.sliderBall = color;
+ break;
+ case "SpinnerApproachCircle":
+ skin.spinnerApproachCircle = color;
+ break;
+ case "SongSelectActiveText":
+ skin.songSelectActiveText = color;
+ break;
+ case "SongSelectInactiveText":
+ skin.songSelectInactiveText = color;
+ break;
+ case "StarBreakAdditive":
+ skin.starBreakAdditive = color;
+ break;
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read color '%s' for file '%s'.",
+ line, skinFile.getAbsolutePath()), e);
+ }
+ }
+ if (!colors.isEmpty())
+ skin.combo = colors.toArray(new Color[colors.size()]);
+ break;
+ case "[Fonts]":
+ while ((line = in.readLine()) != null) {
+ line = line.trim();
+ if (!isValidLine(line))
+ continue;
+ if (line.charAt(0) == '[')
+ break;
+ if ((tokens = tokenize(line)) == null)
+ continue;
+ try {
+ switch (tokens[0]) {
+ case "HitCirclePrefix":
+ skin.hitCirclePrefix = tokens[1];
+ break;
+ case "HitCircleOverlap":
+ skin.hitCircleOverlap = Integer.parseInt(tokens[1]);
+ break;
+ case "ScorePrefix":
+ skin.scorePrefix = tokens[1];
+ break;
+ case "ScoreOverlap":
+ skin.scoreOverlap = Integer.parseInt(tokens[1]);
+ break;
+ case "ComboPrefix":
+ skin.comboPrefix = tokens[1];
+ break;
+ case "ComboOverlap":
+ skin.comboOverlap = Integer.parseInt(tokens[1]);
+ break;
+ default:
+ break;
+ }
+ } catch (Exception e) {
+ Log.warn(String.format("Failed to read color '%s' for file '%s'.",
+ line, skinFile.getAbsolutePath()), e);
+ }
+ }
+ break;
+ default:
+ line = in.readLine();
+ break;
+ }
+ }
+ } catch (IOException e) {
+ ErrorHandler.error(String.format("Failed to read file '%s'.", skinFile.getAbsolutePath()), e, false);
+ }
+
+ return skin;
+ }
+
+ /**
+ * Returns false if the line is too short or commented.
+ */
+ private static boolean isValidLine(String line) {
+ return (line.length() > 1 && !line.startsWith("//"));
+ }
+
+ /**
+ * Splits line into two strings: tag, value.
+ * If no ':' character is present, null will be returned.
+ */
+ private static String[] tokenize(String line) {
+ int index = line.indexOf(':');
+ if (index == -1) {
+ Log.debug(String.format("Failed to tokenize line: '%s'.", line));
+ return null;
+ }
+
+ String[] tokens = new String[2];
+ tokens[0] = line.substring(0, index).trim();
+ tokens[1] = line.substring(index + 1).trim();
+ return tokens;
+ }
+}
diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java
index dde1172b..c27f0b67 100644
--- a/src/itdelatrisu/opsu/states/ButtonMenu.java
+++ b/src/itdelatrisu/opsu/states/ButtonMenu.java
@@ -20,17 +20,17 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.ScoreData;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapSetNode;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.util.ArrayList;
import java.util.List;
@@ -66,9 +66,9 @@ public class ButtonMenu extends BasicGameState {
BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) {
@Override
public String[] getTitle(GameContainer container, StateBasedGame game) {
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
- String osuString = (node != null) ? OsuGroupList.get().getBaseNode(node.index).toString() : "";
- return new String[] { osuString, "What do you want to do with this beatmap?" };
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ String beatmapString = (node != null) ? BeatmapSetList.get().getBaseNode(node.index).toString() : "";
+ return new String[] { beatmapString, "What do you want to do with this beatmap?" };
}
@Override
@@ -86,9 +86,9 @@ public class ButtonMenu extends BasicGameState {
BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) {
@Override
public String[] getTitle(GameContainer container, StateBasedGame game) {
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
- String osuString = (node != null) ? node.toString() : "";
- return new String[] { String.format("Are you sure you wish to delete '%s' from disk?", osuString) };
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ String beatmapString = (node != null) ? node.toString() : "";
+ return new String[] { String.format("Are you sure you wish to delete '%s' from disk?", beatmapString) };
}
@Override
@@ -314,10 +314,10 @@ public class ButtonMenu extends BasicGameState {
public void draw(GameContainer container, StateBasedGame game, Graphics g) {
// draw title
if (actualTitle != null) {
- float c = container.getWidth() * 0.02f;
+ float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
int lineHeight = Utils.FONT_LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++)
- Utils.FONT_LARGE.drawString(c, c + (i * lineHeight), actualTitle.get(i), Color.white);
+ Utils.FONT_LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
}
// draw buttons
@@ -451,7 +451,7 @@ public class ButtonMenu extends BasicGameState {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
}
@@ -460,8 +460,8 @@ public class ButtonMenu extends BasicGameState {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
- MenuState ms = (node.osuFileIndex == -1 || node.osuFiles.size() == 1) ?
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ MenuState ms = (node.beatmapIndex == -1 || node.getBeatmapSet().size() == 1) ?
MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, node);
game.enterState(Opsu.STATE_BUTTONMENU);
@@ -478,7 +478,7 @@ public class ButtonMenu extends BasicGameState {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_CONFIRM, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
}
@@ -493,7 +493,7 @@ public class ButtonMenu extends BasicGameState {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
- OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
+ BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_SELECT, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
}
@@ -582,7 +582,7 @@ public class ButtonMenu extends BasicGameState {
private MenuState menuState;
/** The song node to process in the state. */
- private OsuGroupNode node;
+ private BeatmapSetNode node;
/** The score data to process in the state. */
private ScoreData scoreData;
@@ -691,7 +691,7 @@ public class ButtonMenu extends BasicGameState {
* @param menuState the new menu state
* @param node the song node to process in the state
*/
- public void setMenuState(MenuState menuState, OsuGroupNode node) { setMenuState(menuState, node, null); }
+ public void setMenuState(MenuState menuState, BeatmapSetNode node) { setMenuState(menuState, node, null); }
/**
* Changes the menu state.
@@ -706,7 +706,7 @@ public class ButtonMenu extends BasicGameState {
* @param node the song node to process in the state
* @param scoreData the score scoreData
*/
- private void setMenuState(MenuState menuState, OsuGroupNode node, ScoreData scoreData) {
+ private void setMenuState(MenuState menuState, BeatmapSetNode node, ScoreData scoreData) {
this.menuState = menuState;
this.node = node;
this.scoreData = scoreData;
@@ -715,7 +715,7 @@ public class ButtonMenu extends BasicGameState {
/**
* Returns the song node being processed, or null if none.
*/
- private OsuGroupNode getNode() { return node; }
+ private BeatmapSetNode getNode() { return node; }
/**
* Returns the score data being processed, or null if none.
diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java
index 52d46087..98c08d63 100644
--- a/src/itdelatrisu/opsu/states/DownloadsMenu.java
+++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java
@@ -19,23 +19,25 @@
package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.OsuGroupNode;
-import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
-import itdelatrisu.opsu.downloads.BloodcatServer;
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapSetNode;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode;
-import itdelatrisu.opsu.downloads.DownloadServer;
+import itdelatrisu.opsu.downloads.servers.BloodcatServer;
+import itdelatrisu.opsu.downloads.servers.DownloadServer;
+import itdelatrisu.opsu.downloads.servers.HexideServer;
+import itdelatrisu.opsu.downloads.servers.OsuMirrorServer;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.io.IOException;
@@ -71,8 +73,11 @@ public class DownloadsMenu extends BasicGameState {
/** Minimum time, in milliseconds, that must elapse between queries. */
private static final int MIN_REQUEST_INTERVAL = 300;
- /** The beatmap download server. */
- private DownloadServer server = new BloodcatServer();
+ /** Available beatmap download servers. */
+ private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
+
+ /** The beatmap download server index. */
+ private int serverIndex = 0;
/** The current list of search results. */
private DownloadNode[] resultList;
@@ -138,7 +143,7 @@ public class DownloadsMenu extends BasicGameState {
private MenuButton prevPage, nextPage;
/** Buttons. */
- private MenuButton clearButton, importButton, resetButton, rankedButton;
+ private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
/** Beatmap importing thread. */
private Thread importThread;
@@ -169,12 +174,12 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
float baseX = width * 0.024f;
- float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight();
- float searchWidth = width * 0.35f;
+ float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
+ float searchWidth = width * 0.3f;
// search
searchTimer = SEARCH_DELAY;
- searchResultString = "Type to search!";
+ searchResultString = "Loading data from server...";
search = new TextField(
container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
@@ -200,8 +205,10 @@ public class DownloadsMenu extends BasicGameState {
// buttons
float buttonMarginX = width * 0.004f;
float buttonHeight = height * 0.038f;
- float topButtonWidth = width * 0.14f;
- float lowerButtonWidth = width * 0.12f;
+ float resetWidth = width * 0.085f;
+ float rankedWidth = width * 0.15f;
+ float serverWidth = width * 0.12f;
+ float lowerWidth = width * 0.12f;
float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
Image button = GameImage.MENU_BUTTON_MID.getImage();
@@ -209,25 +216,33 @@ public class DownloadsMenu extends BasicGameState {
Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage();
buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight());
buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight());
- Image topButton = button.getScaledCopy((int) topButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight);
- Image lowerButton = button.getScaledCopy((int) lowerButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight);
- float fullTopButtonWidth = topButton.getWidth() + buttonL.getWidth() + buttonR.getWidth();
- float fullLowerButtonWidth = lowerButton.getWidth() + buttonL.getWidth() + buttonR.getWidth();
- clearButton = new MenuButton(lowerButton, buttonL, buttonR,
- width * 0.75f + buttonMarginX + fullLowerButtonWidth / 2f, lowerButtonY);
- importButton = new MenuButton(lowerButton, buttonL, buttonR,
- width - buttonMarginX - fullLowerButtonWidth / 2f, lowerButtonY);
- resetButton = new MenuButton(topButton, buttonL, buttonR,
- baseX + searchWidth + buttonMarginX + fullTopButtonWidth / 2f, topButtonY);
- rankedButton = new MenuButton(topButton, buttonL, buttonR,
- baseX + searchWidth + buttonMarginX * 2f + fullTopButtonWidth * 3 / 2f, topButtonY);
+ int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
+ Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
+ Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
+ Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
+ Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
+ float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
+ float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
+ float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
+ float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
+ clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
+ width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
+ importButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
+ width - buttonMarginX - lowerButtonWidth / 2f, lowerButtonY);
+ resetButton = new MenuButton(resetButtonImage, buttonL, buttonR,
+ baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
+ rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
+ baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
+ serverButton = new MenuButton(serverButtonImage, buttonL, buttonR,
+ baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY);
clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white);
- resetButton.setText("Reset Search", Utils.FONT_MEDIUM, Color.white);
+ resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white);
clearButton.setHoverFade();
importButton.setHoverFade();
resetButton.setHoverFade();
rankedButton.setHoverFade();
+ serverButton.setHoverFade();
}
@Override
@@ -241,7 +256,7 @@ public class DownloadsMenu extends BasicGameState {
GameImage.SEARCH_BG.getImage().draw();
// title
- Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.04f, "Download Beatmaps!", Color.white);
+ Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
// search
g.setColor(Color.white);
@@ -317,6 +332,8 @@ public class DownloadsMenu extends BasicGameState {
resetButton.draw(Color.red);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
rankedButton.draw(Color.magenta);
+ serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
+ serverButton.draw(Color.blue);
// importing beatmaps
if (importThread != null) {
@@ -348,6 +365,7 @@ public class DownloadsMenu extends BasicGameState {
importButton.hoverUpdate(delta, mouseX, mouseY);
resetButton.hoverUpdate(delta, mouseX, mouseY);
rankedButton.hoverUpdate(delta, mouseX, mouseY);
+ serverButton.hoverUpdate(delta, mouseX, mouseY);
// focus timer
if (focusResult != -1 && focusTimer < FOCUS_DELAY)
@@ -361,7 +379,9 @@ public class DownloadsMenu extends BasicGameState {
searchTimerReset = false;
final String query = search.getText().trim().toLowerCase();
- if (lastQuery == null || !query.equals(lastQuery)) {
+ final DownloadServer server = SERVERS[serverIndex];
+ if ((lastQuery == null || !query.equals(lastQuery)) &&
+ (query.length() == 0 || query.length() >= server.minQueryLength())) {
lastQuery = query;
lastQueryDir = pageDir;
@@ -409,7 +429,7 @@ public class DownloadsMenu extends BasicGameState {
else {
if (query.isEmpty())
searchResultString = "Type to search!";
- else if (totalResults == 0)
+ else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found.";
else
searchResultString = String.format("%d result%s found!",
@@ -427,6 +447,14 @@ public class DownloadsMenu extends BasicGameState {
queryThread.start();
}
}
+
+ // tooltips
+ if (resetButton.contains(mouseX, mouseY))
+ UI.updateTooltip(delta, "Reset the current search.", false);
+ else if (rankedButton.contains(mouseX, mouseY))
+ UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true);
+ else if (serverButton.contains(mouseX, mouseY))
+ UI.updateTooltip(delta, "Select a download server.", false);
}
@Override
@@ -463,7 +491,7 @@ public class DownloadsMenu extends BasicGameState {
final DownloadNode node = nodes[index];
// check if map is already loaded
- boolean isLoaded = OsuGroupList.get().containsBeatmapSetID(node.getID());
+ boolean isLoaded = BeatmapSetList.get().containsBeatmapSetID(node.getID());
// track preview
if (DownloadNode.resultIconContains(x, y, i)) {
@@ -481,7 +509,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// play preview
try {
- final URL url = new URL(server.getPreviewURL(node.getID()));
+ final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
MusicController.pause();
new Thread() {
@Override
@@ -525,9 +553,13 @@ public class DownloadsMenu extends BasicGameState {
} else {
// start download
if (!DownloadList.get().contains(node.getID())) {
- DownloadList.get().addNode(node);
- node.createDownload(server);
- node.getDownload().start();
+ node.createDownload(SERVERS[serverIndex]);
+ if (node.getDownload() == null)
+ UI.sendBarNotification("The download could not be started.");
+ else {
+ DownloadList.get().addNode(node);
+ node.getDownload().start();
+ }
}
}
} else {
@@ -584,15 +616,15 @@ public class DownloadsMenu extends BasicGameState {
// invoke unpacker and parser
File[] dirs = OszUnpacker.unpackAllFiles(Options.getOSZDir(), Options.getBeatmapDir());
if (dirs != null && dirs.length > 0) {
- OsuGroupNode node = OsuParser.parseDirectories(dirs);
+ BeatmapSetNode node = BeatmapParser.parseDirectories(dirs);
if (node != null) {
// stop preview
previewID = -1;
SoundController.stopTrack();
// initialize song list
- OsuGroupList.get().reset();
- OsuGroupList.get().init();
+ BeatmapSetList.get().reset();
+ BeatmapSetList.get().init();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(node, -1, true, true);
// send notification
@@ -624,6 +656,22 @@ public class DownloadsMenu extends BasicGameState {
resetSearchTimer();
return;
}
+ if (serverButton.contains(x, y)) {
+ SoundController.playSound(SoundEffect.MENUCLICK);
+ resultList = null;
+ startResult = 0;
+ focusResult = -1;
+ totalResults = 0;
+ page = 0;
+ pageResultTotal = 1;
+ pageDir = Page.RESET;
+ searchResultString = "Loading data from server...";
+ serverIndex = (serverIndex + 1) % SERVERS.length;
+ lastQuery = null;
+ pageDir = Page.RESET;
+ resetSearchTimer();
+ return;
+ }
// downloads
if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) {
@@ -700,7 +748,7 @@ public class DownloadsMenu extends BasicGameState {
switch (key) {
case Input.KEY_ESCAPE:
if (importThread != null) {
- // beatmap importing: stop parsing OsuFiles by sending interrupt to OsuParser
+ // beatmap importing: stop parsing beatmaps by sending interrupt to BeatmapParser
importThread.interrupt();
} else if (!search.getText().isEmpty()) {
// clear search text
@@ -755,6 +803,7 @@ public class DownloadsMenu extends BasicGameState {
importButton.resetHover();
resetButton.resetHover();
rankedButton.resetHover();
+ serverButton.resetHover();
focusResult = -1;
startResult = 0;
startDownloadIndex = 0;
diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java
index 4e6a4479..acb371c4 100644
--- a/src/itdelatrisu/opsu/states/Game.java
+++ b/src/itdelatrisu/opsu/states/Game.java
@@ -22,29 +22,32 @@ import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.OsuHitObject;
-import itdelatrisu.opsu.OsuParser;
-import itdelatrisu.opsu.OsuTimingPoint;
import itdelatrisu.opsu.ScoreData;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
-import itdelatrisu.opsu.db.OsuDB;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.beatmap.TimingPoint;
+import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.objects.Circle;
import itdelatrisu.opsu.objects.DummyObject;
-import itdelatrisu.opsu.objects.HitObject;
+import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
+import itdelatrisu.opsu.objects.curves.Curve;
+import itdelatrisu.opsu.render.FrameBufferCache;
+import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.util.LinkedList;
@@ -95,17 +98,17 @@ public class Game extends BasicGameState {
/** Stack position offset modifier. */
private static final float STACK_OFFSET_MODIFIER = 0.05f;
- /** The associated OsuFile object. */
- private OsuFile osu;
+ /** The associated beatmap. */
+ private Beatmap beatmap;
/** The associated GameData object. */
private GameData data;
- /** Current hit object index in OsuHitObject[] array. */
+ /** Current hit object index (in both hit object arrays). */
private int objectIndex = 0;
- /** The map's HitObjects, indexed by objectIndex. */
- private HitObject[] hitObjects;
+ /** The map's game objects, indexed by objectIndex. */
+ private GameObject[] gameObjects;
/** Delay time, in milliseconds, before song starts. */
private int leadInTime;
@@ -211,6 +214,9 @@ public class Game extends BasicGameState {
/** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed;
+ /** Playback speed (used in replays and "auto" mod). */
+ private PlaybackSpeed playbackSpeed;
+
// game-related variables
private GameContainer container;
private StateBasedGame game;
@@ -254,7 +260,7 @@ public class Game extends BasicGameState {
trackPosition = pauseTime;
else if (deathTime > -1) // "Easy" mod: health bar increasing
trackPosition = deathTime;
- int firstObjectTime = osu.objects[0].getTime();
+ int firstObjectTime = beatmap.objects[0].getTime();
int timeDiff = firstObjectTime - trackPosition;
g.setBackground(Color.black);
@@ -269,11 +275,11 @@ public class Game extends BasicGameState {
float dimLevel = Options.getBackgroundDim();
if (trackPosition < firstObjectTime) {
if (timeDiff < approachTime)
- dimLevel += (1f - dimLevel) * ((float) timeDiff / Math.min(approachTime, firstObjectTime));
+ dimLevel += (1f - dimLevel) * ((float) timeDiff / approachTime);
else
dimLevel = 1f;
}
- if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) {
+ if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel);
playfield.draw();
@@ -292,51 +298,51 @@ public class Game extends BasicGameState {
float[] autoXY = null;
if (isLeadIn()) {
// lead-in
- float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
+ float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
autoMouseY = (int) (height / (2f - progress));
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
// before first object
timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) {
- float[] xy = hitObjects[0].getPointAt(trackPosition);
- autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / Math.min(approachTime, firstObjectTime)));
+ float[] xy = gameObjects[0].getPointAt(trackPosition);
+ autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
}
- } else if (objectIndex < osu.objects.length) {
+ } else if (objectIndex < beatmap.objects.length) {
// normal object
- int objectTime = osu.objects[objectIndex].getTime();
+ int objectTime = beatmap.objects[objectIndex].getTime();
if (trackPosition < objectTime) {
- float[] xyStart = hitObjects[objectIndex - 1].getPointAt(trackPosition);
- int startTime = hitObjects[objectIndex - 1].getEndTime();
- if (osu.breaks != null && breakIndex < osu.breaks.size()) {
+ float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
+ int startTime = gameObjects[objectIndex - 1].getEndTime();
+ if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
// starting a break: keep cursor at previous hit object position
- if (breakTime > 0 || objectTime > osu.breaks.get(breakIndex))
+ if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
autoXY = xyStart;
// after a break ends: move startTime to break end time
else if (breakIndex > 1) {
- int lastBreakEndTime = osu.breaks.get(breakIndex - 1);
+ int lastBreakEndTime = beatmap.breaks.get(breakIndex - 1);
if (objectTime > lastBreakEndTime && startTime < lastBreakEndTime)
startTime = lastBreakEndTime;
}
}
if (autoXY == null) {
- float[] xyEnd = hitObjects[objectIndex].getPointAt(trackPosition);
+ float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime;
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300];
- if ((osu.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) ||
- (osu.objects[objectIndex - 1].isCircle() && trackPosition - osu.objects[objectIndex - 1].getTime() < offset300))
+ if ((beatmap.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) ||
+ (beatmap.objects[objectIndex - 1].isCircle() && trackPosition - beatmap.objects[objectIndex - 1].getTime() < offset300))
autoMousePressed = true;
}
} else {
- autoXY = hitObjects[objectIndex].getPointAt(trackPosition);
+ autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true;
}
} else {
// last object
- autoXY = hitObjects[objectIndex - 1].getPointAt(trackPosition);
+ autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
}
// set mouse coordinates
@@ -388,12 +394,12 @@ public class Game extends BasicGameState {
}
// break periods
- if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
- int endTime = osu.breaks.get(breakIndex);
+ if (beatmap.breaks != null && breakIndex < beatmap.breaks.size() && breakTime > 0) {
+ int endTime = beatmap.breaks.get(breakIndex);
int breakLength = endTime - breakTime;
// letterbox effect (black bars on top/bottom)
- if (osu.letterboxInBreaks && breakLength >= 4000) {
+ if (beatmap.letterboxInBreaks && breakLength >= 4000) {
g.setColor(Color.black);
g.fillRect(0, 0, width, height * 0.125f);
g.fillRect(0, height * 0.875f, width, height * 0.125f);
@@ -441,7 +447,7 @@ public class Game extends BasicGameState {
// skip beginning
if (objectIndex == 0 &&
- trackPosition < osu.objects[0].getTime() - SKIP_OFFSET)
+ trackPosition < beatmap.objects[0].getTime() - SKIP_OFFSET)
skipButton.draw();
// show retries
@@ -465,40 +471,41 @@ public class Game extends BasicGameState {
trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in
// countdown
- if (osu.countdown > 0) { // TODO: implement half/double rate settings
+ if (beatmap.countdown > 0) {
+ float speedModifier = GameMod.getSpeedMultiplier() * playbackSpeed.getModifier();
timeDiff = firstObjectTime - trackPosition;
- if (timeDiff >= 500 && timeDiff < 3000) {
- if (timeDiff >= 1500) {
+ if (timeDiff >= 500 * speedModifier && timeDiff < 3000 * speedModifier) {
+ if (timeDiff >= 1500 * speedModifier) {
GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2);
if (!countdownReadySound) {
SoundController.playSound(SoundEffect.READY);
countdownReadySound = true;
}
}
- if (timeDiff < 2000) {
+ if (timeDiff < 2000 * speedModifier) {
GameImage.COUNTDOWN_3.getImage().draw(0, 0);
if (!countdown3Sound) {
SoundController.playSound(SoundEffect.COUNT3);
countdown3Sound = true;
}
}
- if (timeDiff < 1500) {
+ if (timeDiff < 1500 * speedModifier) {
GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0);
if (!countdown2Sound) {
SoundController.playSound(SoundEffect.COUNT2);
countdown2Sound = true;
}
}
- if (timeDiff < 1000) {
+ if (timeDiff < 1000 * speedModifier) {
GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2);
if (!countdown1Sound) {
SoundController.playSound(SoundEffect.COUNT1);
countdown1Sound = true;
}
}
- } else if (timeDiff >= -500 && timeDiff < 500) {
+ } else if (timeDiff >= -500 * speedModifier && timeDiff < 500 * speedModifier) {
Image go = GameImage.COUNTDOWN_GO.getImage();
- go.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1);
+ go.setAlpha((timeDiff < 0) ? 1 - (timeDiff / speedModifier / -500f) : 1);
go.drawCentered(width / 2, height / 2);
if (!countdownGoSound) {
SoundController.playSound(SoundEffect.GO);
@@ -515,6 +522,10 @@ public class Game extends BasicGameState {
if (GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
+ // draw replay speed button
+ if (isReplay || GameMod.AUTO.isActive())
+ playbackSpeed.getButton().draw();
+
// returning from pause screen
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
// darken the screen
@@ -539,7 +550,6 @@ public class Game extends BasicGameState {
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
else
UI.draw(g);
-
}
@Override
@@ -548,6 +558,8 @@ public class Game extends BasicGameState {
UI.update(delta);
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
skipButton.hoverUpdate(delta, mouseX, mouseY);
+ if (isReplay || GameMod.AUTO.isActive())
+ playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition();
// returning from pause screen: must click previous mouse position
@@ -660,10 +672,10 @@ public class Game extends BasicGameState {
}
// map complete!
- if (objectIndex >= hitObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
+ if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result
- if (MusicController.trackEnded() && objectIndex < hitObjects.length)
- hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
+ if (MusicController.trackEnded() && objectIndex < gameObjects.length)
+ gameObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
// if checkpoint used, skip ranking screen
if (checkpointLoaded)
@@ -681,11 +693,11 @@ public class Game extends BasicGameState {
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0));
- Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu);
+ Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), beatmap);
if (r != null && !unranked)
r.save();
}
- ScoreData score = data.getScoreData(osu);
+ ScoreData score = data.getScoreData(beatmap);
// add score to database
if (!unranked && !isReplay)
@@ -697,23 +709,21 @@ public class Game extends BasicGameState {
}
// timing points
- if (timingPointIndex < osu.timingPoints.size()) {
- OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
+ if (timingPointIndex < beatmap.timingPoints.size()) {
+ TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
if (trackPosition >= timingPoint.getTime()) {
- setBeatLength(timingPoint);
- HitSound.setDefaultSampleSet(timingPoint.getSampleType());
- SoundController.setSampleVolume(timingPoint.getSampleVolume());
+ setBeatLength(timingPoint, true);
timingPointIndex++;
}
}
// song beginning
- if (objectIndex == 0 && trackPosition < osu.objects[0].getTime())
+ if (objectIndex == 0 && trackPosition < beatmap.objects[0].getTime())
return; // nothing to do here
// break periods
- if (osu.breaks != null && breakIndex < osu.breaks.size()) {
- int breakValue = osu.breaks.get(breakIndex);
+ if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
+ int breakValue = beatmap.breaks.get(breakIndex);
if (breakTime > 0) { // in a break period
if (trackPosition < breakValue)
return;
@@ -765,13 +775,13 @@ public class Game extends BasicGameState {
// update objects (loop in unlikely event of any skipped indexes)
boolean keyPressed = keys != ReplayFrame.KEY_NONE;
- while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) {
+ while (objectIndex < gameObjects.length && trackPosition > beatmap.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < hitObjects.length &&
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]);
// update hit object and check completion status
- if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
+ if (gameObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
objectIndex++; // done, so increment object index
else
break;
@@ -807,7 +817,7 @@ public class Game extends BasicGameState {
}
// pause game
- if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) {
+ if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausePulse = 0f;
@@ -824,7 +834,7 @@ public class Game extends BasicGameState {
// restart
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
try {
- if (trackPosition < osu.objects[0].getTime())
+ if (trackPosition < beatmap.objects[0].getTime())
retries--; // don't count this retry (cancel out later increment)
restart = Restart.MANUAL;
enter(container, game);
@@ -851,7 +861,7 @@ public class Game extends BasicGameState {
// load checkpoint
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
int checkpoint = Options.getCheckpoint();
- if (checkpoint == 0 || checkpoint > osu.endTime)
+ if (checkpoint == 0 || checkpoint > beatmap.endTime)
break; // invalid checkpoint
try {
restart = Restart.MANUAL;
@@ -866,11 +876,12 @@ public class Game extends BasicGameState {
// skip to checkpoint
MusicController.setPosition(checkpoint);
- while (objectIndex < hitObjects.length &&
- osu.objects[objectIndex++].getTime() <= checkpoint)
+ MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
+ while (objectIndex < gameObjects.length &&
+ beatmap.objects[objectIndex++].getTime() <= checkpoint)
;
objectIndex--;
- lastReplayTime = osu.objects[objectIndex].getTime();
+ lastReplayTime = beatmap.objects[objectIndex].getTime();
} catch (SlickException e) {
ErrorHandler.error("Failed to load checkpoint.", e, false);
}
@@ -896,13 +907,13 @@ public class Game extends BasicGameState {
@Override
public void mousePressed(int button, int x, int y) {
- if (Options.isMouseDisabled())
- return;
-
// watching replay
- if (isReplay) {
- // only allow skip button
- if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y))
+ if (isReplay || GameMod.AUTO.isActive()) {
+ if (button == Input.MOUSE_MIDDLE_BUTTON)
+ return;
+
+ // skip button
+ if (skipButton.contains(x, y))
skipIntro();
if(y < 50){
float pos = (float)x / width * osu.endTime;
@@ -912,10 +923,13 @@ public class Game extends BasicGameState {
return;
}
+ if (Options.isMouseDisabled())
+ return;
+
// mouse wheel: pause the game
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition();
- if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) {
+ if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = x;
pausedMouseY = y;
pausePulse = 0f;
@@ -1031,8 +1045,11 @@ public class Game extends BasicGameState {
throws SlickException {
UI.enter();
- if (osu == null || osu.objects == null)
- throw new RuntimeException("Running game with no OsuFile loaded.");
+ if (beatmap == null || beatmap.objects == null)
+ throw new RuntimeException("Running game with no beatmap loaded.");
+
+ // free all previously cached hitobject to framebuffer mappings if some still exist
+ FrameBufferCache.getInstance().freeMap();
// grab the mouse (not working for touchscreen)
// container.setMouseGrabbed(true);
@@ -1053,10 +1070,14 @@ public class Game extends BasicGameState {
// reset game data
resetGameData();
- // needs to play before setting position to resume without lag later
- MusicController.play(false);
- MusicController.setPosition(0);
- MusicController.pause();
+ // load the first timingPoint for stacking
+ if (!beatmap.timingPoints.isEmpty()) {
+ TimingPoint timingPoint = beatmap.timingPoints.get(0);
+ if (!timingPoint.isInherited()) {
+ setBeatLength(timingPoint, true);
+ timingPointIndex++;
+ }
+ }
if (!osu.timingPoints.isEmpty()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(0);
@@ -1067,39 +1088,39 @@ public class Game extends BasicGameState {
hitObjects = new HitObject[osu.objects.length];
// initialize object maps
- for (int i = 0; i < osu.objects.length; i++) {
- OsuHitObject hitObject = osu.objects[i];
+ Color[] combo = beatmap.getComboColors();
+ for (int i = 0; i < beatmap.objects.length; i++) {
+ HitObject hitObject = beatmap.objects[i];
// is this the last note in the combo?
boolean comboEnd = false;
if (i + 1 >= osu.objects.length || osu.objects[i + 1].isNewCombo())
comboEnd = true;
- Color color = osu.combo[hitObject.getComboIndex()];
+ Color color = combo[hitObject.getComboIndex()];
// pass beatLength to hit objects
int hitObjectTime = hitObject.getTime();
- int timingPointIndex = 0;
- while (timingPointIndex < osu.timingPoints.size()) {
- OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
+ while (timingPointIndex < beatmap.timingPoints.size()) {
+ TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
if (timingPoint.getTime() > hitObjectTime)
break;
- setBeatLength(timingPoint);
+ setBeatLength(timingPoint, false);
timingPointIndex++;
}
try {
if (hitObject.isCircle())
- hitObjects[i] = new Circle(hitObject, this, data, color, comboEnd);
+ gameObjects[i] = new Circle(hitObject, this, data, color, comboEnd);
else if (hitObject.isSlider())
- hitObjects[i] = new Slider(hitObject, this, data, color, comboEnd);
+ gameObjects[i] = new Slider(hitObject, this, data, color, comboEnd);
else if (hitObject.isSpinner())
- hitObjects[i] = new Spinner(hitObject, this, data);
+ gameObjects[i] = new Spinner(hitObject, this, data);
} catch (Exception e) {
- // try to handle the error gracefully: substitute in a dummy HitObject
+ // try to handle the error gracefully: substitute in a dummy GameObject
ErrorHandler.error(String.format("Failed to create %s at index %d:\n%s",
hitObject.getTypeName(), i, hitObject.toString()), e, true);
- hitObjects[i] = new DummyObject(hitObject);
+ gameObjects[i] = new DummyObject(hitObject);
continue;
}
}
@@ -1108,19 +1129,19 @@ public class Game extends BasicGameState {
calculateStacks();
// load the first timingPoint
- if (!osu.timingPoints.isEmpty()) {
- OsuTimingPoint timingPoint = osu.timingPoints.get(0);
+ timingPointIndex = 0;
+ beatLengthBase = beatLength = 1;
+ if (!beatmap.timingPoints.isEmpty()) {
+ TimingPoint timingPoint = beatmap.timingPoints.get(0);
if (!timingPoint.isInherited()) {
- beatLengthBase = beatLength = timingPoint.getBeatLength();
- HitSound.setDefaultSampleSet(timingPoint.getSampleType());
- SoundController.setSampleVolume(timingPoint.getSampleVolume());
+ setBeatLength(timingPoint, true);
timingPointIndex++;
}
}
// unhide cursor for "auto" mod and replays
if (GameMod.AUTO.isActive() || isReplay)
- UI.showCursor();
+ UI.getCursor().show();
// load replay frames
if (isReplay) {
@@ -1156,11 +1177,20 @@ public class Game extends BasicGameState {
replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0));
}
- leadInTime = osu.audioLeadIn + approachTime;
+ leadInTime = beatmap.audioLeadIn + approachTime;
restart = Restart.FALSE;
+
+ // needs to play before setting position to resume without lag later
+ MusicController.play(false);
+ MusicController.setPosition(0);
+ MusicController.setPitch(GameMod.getSpeedMultiplier());
+ MusicController.pause();
}
skipButton.resetHover();
+ if (isReplay || GameMod.AUTO.isActive())
+ playbackSpeed.getButton().resetHover();
+ MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
}
@Override
@@ -1170,11 +1200,14 @@ public class Game extends BasicGameState {
// re-hide cursor
if (GameMod.AUTO.isActive() || isReplay)
- UI.hideCursor();
+ UI.getCursor().hide();
// replays
if (isReplay)
GameMod.loadModState(previousMods);
+
+ // reset playback speed
+ MusicController.setPitch(1f);
}
/**
@@ -1185,29 +1218,29 @@ public class Game extends BasicGameState {
private void drawHitObjects(Graphics g, int trackPosition) {
// include previous object in follow points
int lastObjectIndex = -1;
- if (objectIndex > 0 && objectIndex < osu.objects.length &&
- trackPosition < osu.objects[objectIndex].getTime() && !osu.objects[objectIndex - 1].isSpinner())
+ if (objectIndex > 0 && objectIndex < beatmap.objects.length &&
+ trackPosition < beatmap.objects[objectIndex].getTime() && !beatmap.objects[objectIndex - 1].isSpinner())
lastObjectIndex = objectIndex - 1;
// draw hit objects in reverse order, or else overlapping objects are unreadable
Stack stack = new Stack();
- for (int index = objectIndex; index < hitObjects.length && osu.objects[index].getTime() < trackPosition + approachTime; index++) {
+ for (int index = objectIndex; index < gameObjects.length && beatmap.objects[index].getTime() < trackPosition + approachTime; index++) {
stack.add(index);
// draw follow points
if (!Options.isFollowPointEnabled())
continue;
- if (osu.objects[index].isSpinner()) {
+ if (beatmap.objects[index].isSpinner()) {
lastObjectIndex = -1;
continue;
}
- if (lastObjectIndex != -1 && !osu.objects[index].isNewCombo()) {
+ if (lastObjectIndex != -1 && !beatmap.objects[index].isNewCombo()) {
// calculate points
final int followPointInterval = container.getHeight() / 14;
- int lastObjectEndTime = hitObjects[lastObjectIndex].getEndTime() + 1;
- int objectStartTime = osu.objects[index].getTime();
- float[] startXY = hitObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
- float[] endXY = hitObjects[index].getPointAt(objectStartTime);
+ int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
+ int objectStartTime = beatmap.objects[index].getTime();
+ float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
+ float[] endXY = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endXY[0] - startXY[0];
float yDiff = endXY[1] - startXY[1];
float dist = (float) Math.hypot(xDiff, yDiff);
@@ -1254,29 +1287,32 @@ public class Game extends BasicGameState {
}
while (!stack.isEmpty())
- hitObjects[stack.pop()].draw(g, trackPosition);
+ gameObjects[stack.pop()].draw(g, trackPosition);
// draw OsuHitObjectResult objects
data.drawHitResults(trackPosition);
}
/**
- * Loads all required data from an OsuFile.
- * @param osu the OsuFile to load
+ * Loads all required data from a beatmap.
+ * @param beatmap the beatmap to load
*/
- public void loadOsuFile(OsuFile osu) {
- this.osu = osu;
- Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
- if (osu.timingPoints == null || osu.combo == null)
- OsuDB.load(osu, OsuDB.LOAD_ARRAY);
- OsuParser.parseHitObjects(osu);
- HitSound.setDefaultSampleSet(osu.sampleSet);
+ public void loadBeatmap(Beatmap beatmap) {
+ this.beatmap = beatmap;
+ Display.setTitle(String.format("%s - %s", game.getTitle(), beatmap.toString()));
+ if (beatmap.timingPoints == null)
+ BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
+ BeatmapParser.parseHitObjects(beatmap);
+ HitSound.setDefaultSampleSet(beatmap.sampleSet);
}
/**
* Resets all game data and structures.
*/
public void resetGameData() {
+ //conflict
+ gameObjects = new GameObject[beatmap.objects.length];
+ //
data.clear();
objectIndex = 0;
breakIndex = 0;
@@ -1301,16 +1337,17 @@ public class Game extends BasicGameState {
autoMouseY = 0;
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
+ playbackSpeed = PlaybackSpeed.NORMAL;
System.gc();
}
/**
* Skips the beginning of a track.
- * @return true if skipped, false otherwise
+ * @return {@code true} if skipped, {@code false} otherwise
*/
private synchronized boolean skipIntro() {
- int firstObjectTime = osu.objects[0].getTime();
+ int firstObjectTime = beatmap.objects[0].getTime();
int trackPosition = MusicController.getPosition();
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
if (isLeadIn()) {
@@ -1318,6 +1355,7 @@ public class Game extends BasicGameState {
MusicController.resume();
}
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
+ MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
replaySkipTime = (isReplay) ? -1 : trackPosition;
if (isReplay) {
replayX = (int) skipButton.getX();
@@ -1337,7 +1375,7 @@ public class Game extends BasicGameState {
int height = container.getHeight();
// set images
- File parent = osu.getFile().getParentFile();
+ File parent = beatmap.getFile().getParentFile();
for (GameImage img : GameImage.values()) {
if (img.isSkinnable()) {
img.setDefaultImage();
@@ -1365,26 +1403,11 @@ public class Game extends BasicGameState {
*/
private void setMapModifiers() {
// map-based properties, re-initialized each game
- float circleSize = osu.circleSize;
- float approachRate = osu.approachRate;
- float overallDifficulty = osu.overallDifficulty;
- float HPDrainRate = osu.HPDrainRate;
-
- // "Hard Rock" modifiers
- if (GameMod.HARD_ROCK.isActive()) {
- circleSize = Math.min(circleSize * 1.4f, 10);
- approachRate = Math.min(approachRate * 1.4f, 10);
- overallDifficulty = Math.min(overallDifficulty * 1.4f, 10);
- HPDrainRate = Math.min(HPDrainRate * 1.4f, 10);
- }
-
- // "Easy" modifiers
- else if (GameMod.EASY.isActive()) {
- circleSize /= 2f;
- approachRate /= 2f;
- overallDifficulty /= 2f;
- HPDrainRate /= 2f;
- }
+ float multiplier = GameMod.getDifficultyMultiplier();
+ float circleSize = Math.min(beatmap.circleSize * multiplier, 10f);
+ float approachRate = Math.min(beatmap.approachRate * multiplier, 10f);
+ float overallDifficulty = Math.min(beatmap.overallDifficulty * multiplier, 10f);
+ float HPDrainRate = Math.min(beatmap.HPDrainRate * multiplier, 10f);
// fixed difficulty overrides
if (Options.getFixedCS() > 0f)
@@ -1399,12 +1422,14 @@ public class Game extends BasicGameState {
// Stack modifier scales with hit object size
// StackOffset = HitObjectRadius / 10
int diameter = (int) (104 - (circleSize * 8));
- OsuHitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
+ HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
// initialize objects
Circle.init(container, circleSize);
- Slider.init(container, circleSize, osu);
+ Slider.init(container, circleSize, beatmap);
Spinner.init(container);
+ Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
+ Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
// approachRate (hit object approach time)
if (approachRate < 5)
@@ -1436,9 +1461,14 @@ public class Game extends BasicGameState {
}
/**
- * Sets/returns whether entering the state will restart it.
+ * Sets the restart state.
+ * @param restart the new restart state
*/
public void setRestart(Restart restart) { this.restart = restart; }
+
+ /**
+ * Returns the current restart state.
+ */
public Restart getRestart() { return restart; }
/**
@@ -1463,12 +1493,18 @@ public class Game extends BasicGameState {
/**
* Sets the beat length fields based on a given timing point.
+ * @param timingPoint the timing point
+ * @param setSampleSet whether to set the hit sample set based on the timing point
*/
- private void setBeatLength(OsuTimingPoint timingPoint) {
+ private void setBeatLength(TimingPoint timingPoint, boolean setSampleSet) {
if (!timingPoint.isInherited())
beatLengthBase = beatLength = timingPoint.getBeatLength();
else
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
+ if (setSampleSet) {
+ HitSound.setDefaultSampleSet(timingPoint.getSampleType());
+ SoundController.setSampleVolume(timingPoint.getSampleVolume());
+ }
}
/**
@@ -1540,18 +1576,18 @@ public class Game extends BasicGameState {
*/
private void sendGameKeyPress(int keys, int x, int y, int trackPosition) {
System.out.println("Game Key Pressed"+keys+" "+x+" "+y+" "+objectIndex);
- if (objectIndex >= hitObjects.length) // nothing to do here
+ if (objectIndex >= gameObjects.length) // nothing to do here
return;
- OsuHitObject hitObject = osu.objects[objectIndex];
+ HitObject hitObject = beatmap.objects[objectIndex];
// circles
- if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition))
+ if (hitObject.isCircle() && gameObjects[objectIndex].mousePressed(x, y, trackPosition))
objectIndex++; // circle hit
// sliders
else if (hitObject.isSlider())
- hitObjects[objectIndex].mousePressed(x, y, trackPosition);
+ gameObjects[objectIndex].mousePressed(x, y, trackPosition);
}
/**
@@ -1565,8 +1601,8 @@ public class Game extends BasicGameState {
private ReplayFrame addReplayFrame(int x, int y, int keys, int time) {
int timeDiff = time - lastReplayTime;
lastReplayTime = time;
- int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
- int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
+ int cx = (int) ((x - HitObject.getXOffset()) / HitObject.getXMultiplier());
+ int cy = (int) ((y - HitObject.getYOffset()) / HitObject.getYMultiplier());
ReplayFrame frame = new ReplayFrame(timeDiff, time, cx, cy, keys);
if (replayFrames != null)
replayFrames.add(frame);
@@ -1603,14 +1639,14 @@ public class Game extends BasicGameState {
return;
int width = container.getWidth(), height = container.getHeight();
- boolean firstObject = (objectIndex == 0 && trackPosition < osu.objects[0].getTime());
+ boolean firstObject = (objectIndex == 0 && trackPosition < beatmap.objects[0].getTime());
if (isLeadIn()) {
// lead-in: expand area
- float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
+ float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
flashlightRadius = width - (int) ((width - (height * 2 / 3)) * progress);
} else if (firstObject) {
// before first object: shrink area
- int timeDiff = osu.objects[0].getTime() - trackPosition;
+ int timeDiff = beatmap.objects[0].getTime() - trackPosition;
flashlightRadius = width;
if (timeDiff < approachTime) {
float progress = (float) timeDiff / approachTime;
@@ -1626,10 +1662,10 @@ public class Game extends BasicGameState {
targetRadius = height / 2;
else
targetRadius = height / 3;
- if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
+ if (beatmap.breaks != null && breakIndex < beatmap.breaks.size() && breakTime > 0) {
// breaks: expand at beginning, shrink at end
flashlightRadius = targetRadius;
- int endTime = osu.breaks.get(breakIndex);
+ int endTime = beatmap.breaks.get(breakIndex);
int breakLength = endTime - breakTime;
if (breakLength > approachTime * 3) {
float progress = 1f;
@@ -1662,8 +1698,8 @@ public class Game extends BasicGameState {
*/
private void calculateStacks() {
// reverse pass for stack calculation
- for (int i = hitObjects.length - 1; i > 0; i--) {
- OsuHitObject hitObjectI = osu.objects[i];
+ for (int i = gameObjects.length - 1; i > 0; i--) {
+ HitObject hitObjectI = beatmap.objects[i];
// already calculated
if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner())
@@ -1671,33 +1707,33 @@ public class Game extends BasicGameState {
// search for hit objects in stack
for (int n = i - 1; n >= 0; n--) {
- OsuHitObject hitObjectN = osu.objects[n];
+ HitObject hitObjectN = beatmap.objects[n];
if (hitObjectN.isSpinner())
continue;
// check if in range stack calculation
- float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * osu.stackLeniency);
- float timeN = hitObjectN.isSlider() ? hitObjects[n].getEndTime() : hitObjectN.getTime();
+ float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * beatmap.stackLeniency);
+ float timeN = hitObjectN.isSlider() ? gameObjects[n].getEndTime() : hitObjectN.getTime();
if (timeI > timeN)
break;
// possible special case: if slider end in the stack,
// all next hit objects in stack move right down
if (hitObjectN.isSlider()) {
- float[] p1 = hitObjects[i].getPointAt(hitObjectI.getTime());
- float[] p2 = hitObjects[n].getPointAt(hitObjects[n].getEndTime());
+ float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
+ float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
// check if hit object part of this stack
- if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) {
+ if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
int offset = hitObjectI.getStack() - hitObjectN.getStack() + 1;
for (int j = n + 1; j <= i; j++) {
- OsuHitObject hitObjectJ = osu.objects[j];
- p1 = hitObjects[j].getPointAt(hitObjectJ.getTime());
+ HitObject hitObjectJ = beatmap.objects[j];
+ p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
// hit object below slider end
- if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier())
+ if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
hitObjectJ.setStack(hitObjectJ.getStack() - offset);
}
break; // slider end always start of the stack: reset calculation
@@ -1717,9 +1753,9 @@ public class Game extends BasicGameState {
}
// update hit object positions
- for (int i = 0; i < hitObjects.length; i++) {
- if (osu.objects[i].getStack() != 0)
- hitObjects[i].updatePosition();
+ for (int i = 0; i < gameObjects.length; i++) {
+ if (beatmap.objects[i].getStack() != 0)
+ gameObjects[i].updatePosition();
}
}
}
diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java
index 4eec52f5..e73e5a2a 100644
--- a/src/itdelatrisu/opsu/states/GamePauseMenu.java
+++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java
@@ -19,14 +19,14 @@
package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import org.lwjgl.input.Keyboard;
import org.newdawn.slick.Color;
@@ -132,8 +132,9 @@ public class GamePauseMenu extends BasicGameState {
if (gameState.getRestart() == Game.Restart.LOSE) {
SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
- MusicController.playAt(MusicController.getOsuFile().previewTime, true);
- UI.resetCursor();
+ MusicController.playAt(MusicController.getBeatmap().previewTime, true);
+ if (UI.getCursor().isSkinned())
+ UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else {
SoundController.playSound(SoundEffect.MENUBACK);
@@ -183,10 +184,11 @@ public class GamePauseMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
if (loseState)
- MusicController.playAt(MusicController.getOsuFile().previewTime, true);
+ MusicController.playAt(MusicController.getBeatmap().previewTime, true);
else
MusicController.resume();
- UI.resetCursor();
+ if (UI.getCursor().isSkinned())
+ UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
}
diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java
index 327f9c41..4fcc18ec 100644
--- a/src/itdelatrisu/opsu/states/GameRanking.java
+++ b/src/itdelatrisu/opsu/states/GameRanking.java
@@ -20,16 +20,16 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.replay.Replay;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -102,14 +102,14 @@ public class GameRanking extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
- OsuFile osu = MusicController.getOsuFile();
+ Beatmap beatmap = MusicController.getBeatmap();
// background
- if (!osu.drawBG(width, height, 0.7f, true))
+ if (!beatmap.drawBG(width, height, 0.7f, true))
GameImage.PLAYFIELD.getImage().draw(0,0);
// ranking screen elements
- data.drawRankingElements(g, osu);
+ data.drawRankingElements(g, beatmap);
// buttons
replayButton.draw();
@@ -201,8 +201,8 @@ public class GameRanking extends BasicGameState {
}
if (returnToGame) {
- OsuFile osu = MusicController.getOsuFile();
- gameState.loadOsuFile(osu);
+ Beatmap beatmap = MusicController.getBeatmap();
+ gameState.loadBeatmap(beatmap);
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return;
@@ -244,7 +244,8 @@ public class GameRanking extends BasicGameState {
songMenu.resetGameDataOnLoad();
songMenu.resetTrackOnLoad();
}
- UI.resetCursor();
+ if (UI.getCursor().isSkinned())
+ UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java
index 999b4420..53aa372e 100644
--- a/src/itdelatrisu/opsu/states/MainMenu.java
+++ b/src/itdelatrisu/opsu/states/MainMenu.java
@@ -20,20 +20,20 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.MenuButton;
-import itdelatrisu.opsu.MenuButton.Expand;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.OsuGroupNode;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
+import itdelatrisu.opsu.ui.MenuButton.Expand;
import java.awt.Desktop;
import java.io.IOException;
@@ -91,7 +91,7 @@ public class MainMenu extends BasicGameState {
private MenuButton updateButton;
/** Application start time, for drawing the total running time. */
- private long osuStartTime;
+ private long programStartTime;
/** Indexes of previous songs. */
private Stack previous;
@@ -127,7 +127,7 @@ public class MainMenu extends BasicGameState {
this.game = game;
this.input = container.getInput();
- osuStartTime = System.currentTimeMillis();
+ programStartTime = System.currentTimeMillis();
previous = new Stack();
int width = container.getWidth();
@@ -199,9 +199,9 @@ public class MainMenu extends BasicGameState {
int height = container.getHeight();
// draw background
- OsuFile osu = MusicController.getOsuFile();
+ Beatmap beatmap = MusicController.getBeatmap();
if (Options.isDynamicBackgroundEnabled() &&
- osu != null && osu.drawBG(width, height, bgAlpha, true))
+ beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
;
else {
Image bg = GameImage.MENU_BG.getImage();
@@ -240,7 +240,7 @@ public class MainMenu extends BasicGameState {
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white);
- if (!MusicController.isTrackLoading() && osu != null) {
+ if (!MusicController.isTrackLoading() && beatmap != null) {
float musicBarPosition = Math.min((float) MusicController.getPosition() / MusicController.getDuration(), 1f);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4);
}
@@ -270,25 +270,25 @@ public class MainMenu extends BasicGameState {
}
// draw text
- float marginX = width * 0.015f, marginY = height * 0.015f;
+ float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
g.setFont(Utils.FONT_MEDIUM);
- int lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 9 / 10;
+ float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
- OsuGroupList.get().getMapSetCount(), OsuGroupList.get().getMapCount()), marginX, marginY);
+ BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
if (MusicController.isTrackLoading())
- g.drawString("Track loading...", marginX, marginY + lineHeight);
+ g.drawString("Track loading...", marginX, topMarginY + lineHeight);
else if (MusicController.trackExists()) {
if (Options.useUnicodeMetadata()) // load glyphs
- Utils.loadGlyphs(Utils.FONT_MEDIUM, osu.titleUnicode, osu.artistUnicode);
- g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, marginY + lineHeight);
- g.drawString(String.format("%s: %s", osu.getArtist(), osu.getTitle()), marginX + 25, marginY + (lineHeight * 2));
+ Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
+ g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
+ g.drawString(String.format("%s: %s", beatmap.getArtist(), beatmap.getTitle()), marginX + 25, topMarginY + (lineHeight * 2));
}
g.drawString(String.format("opsu! has been running for %s.",
- Utils.getTimeString((int) (System.currentTimeMillis() - osuStartTime) / 1000)),
- marginX, height - marginY - (lineHeight * 2));
+ Utils.getTimeString((int) (System.currentTimeMillis() - programStartTime) / 1000)),
+ marginX, height - bottomMarginY - (lineHeight * 2));
g.drawString(String.format("It is currently %s.",
new SimpleDateFormat("h:mm a").format(new Date())),
- marginX, height - marginY - lineHeight);
+ marginX, height - bottomMarginY - lineHeight);
UI.draw(g);
}
@@ -455,7 +455,7 @@ public class MainMenu extends BasicGameState {
} else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
- menu.setFocus(OsuGroupList.get().getBaseNode(previous.pop()), -1, true, false);
+ menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
if (Options.isDynamicBackgroundEnabled())
bgAlpha = 0f;
} else
@@ -603,10 +603,10 @@ public class MainMenu extends BasicGameState {
private void nextTrack() {
boolean isTheme = MusicController.isThemePlaying();
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
- OsuGroupNode node = menu.setFocus(OsuGroupList.get().getRandomNode(), -1, true, false);
+ BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false);
boolean sameAudio = false;
if (node != null) {
- sameAudio = MusicController.getOsuFile().audioFilename.equals(node.osuFiles.get(0).audioFilename);
+ sameAudio = MusicController.getBeatmap().audioFilename.equals(node.getBeatmapSet().get(0).audioFilename);
if (!isTheme && !sameAudio)
previous.add(node.index);
}
@@ -619,7 +619,7 @@ public class MainMenu extends BasicGameState {
*/
private void enterSongMenu() {
int state = Opsu.STATE_SONGMENU;
- if (OsuGroupList.get().getMapSetCount() == 0) {
+ if (BeatmapSetList.get().getMapSetCount() == 0) {
((DownloadsMenu) game.getState(Opsu.STATE_DOWNLOADSMENU)).notifyOnLoad("Download some beatmaps to get started!");
state = Opsu.STATE_DOWNLOADSMENU;
}
diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java
index ce7953d5..86d5fdb7 100644
--- a/src/itdelatrisu/opsu/states/OptionsMenu.java
+++ b/src/itdelatrisu/opsu/states/OptionsMenu.java
@@ -19,15 +19,15 @@
package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Options.GameOption;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.util.Arrays;
import java.util.Collections;
@@ -52,12 +52,14 @@ public class OptionsMenu extends BasicGameState {
DISPLAY ("Display", new GameOption[] {
GameOption.SCREEN_RESOLUTION,
// GameOption.FULLSCREEN,
+ GameOption.SKIN,
GameOption.TARGET_FPS,
GameOption.SHOW_FPS,
GameOption.SHOW_UNICODE,
GameOption.SCREENSHOT_FORMAT,
GameOption.NEW_CURSOR,
GameOption.DYNAMIC_BACKGROUND,
+ GameOption.LOAD_HD_IMAGES,
GameOption.LOAD_VERBOSE
}),
MUSIC ("Music", new GameOption[] {
@@ -90,8 +92,7 @@ public class OptionsMenu extends BasicGameState {
GameOption.FIXED_HP,
GameOption.FIXED_AR,
GameOption.FIXED_OD,
- GameOption.CHECKPOINT,
- GameOption.LOAD_HD_IMAGES
+ GameOption.CHECKPOINT
});
/** Total number of tabs. */
@@ -180,9 +181,9 @@ public class OptionsMenu extends BasicGameState {
// option tabs
Image tabImage = GameImage.MENU_TAB.getImage();
- float tabX = (width / 50) + (tabImage.getWidth() / 2f);
- float tabY = Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
- height * 0.03f + (tabImage.getHeight() / 2f);
+ float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
+ float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
+ height * 0.015f - (tabImage.getHeight() / 2f);
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
for (OptionTab tab : OptionTab.values())
tab.button = new MenuButton(tabImage, tabX + (tab.ordinal() * tabOffset), tabY);
@@ -201,12 +202,16 @@ public class OptionsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
+ float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
// title
- float c = container.getWidth() * 0.02f;
- Utils.FONT_LARGE.drawString(c, c, "Game Options", Color.white);
- Utils.FONT_DEFAULT.drawString(c, c + Utils.FONT_LARGE.getLineHeight() * 0.9f,
- "Click or drag an option to change it.", Color.white);
+ float marginX = width * 0.015f, marginY = height * 0.01f;
+ Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
+ Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
+ "Change the way opsu! behaves", Color.white);
+
+ // background
+ GameImage.OPTIONS_BG.getImage().draw(0, lineY);
// game options
g.setLineWidth(1f);
@@ -235,7 +240,6 @@ public class OptionsMenu extends BasicGameState {
currentTab.getName(), true, false);
g.setColor(Color.white);
g.setLineWidth(2f);
- float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
g.drawLine(0, lineY, width, lineY);
g.resetLineWidth();
@@ -302,7 +306,7 @@ public class OptionsMenu extends BasicGameState {
// options (click only)
GameOption option = getOptionAt(y);
- if (option != GameOption.NULL)
+ if (option != null)
option.click(container);
// special key entry states
@@ -338,7 +342,7 @@ public class OptionsMenu extends BasicGameState {
// options (drag only)
GameOption option = getOptionAt(oldy);
- if (option != GameOption.NULL)
+ if (option != null)
option.drag(container, diff);
}
@@ -425,14 +429,13 @@ public class OptionsMenu extends BasicGameState {
* @return the option, or GameOption.NULL if no such option exists
*/
private GameOption getOptionAt(int y) {
- GameOption option = GameOption.NULL;
-
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
- return option;
+ return null;
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
- if (index < currentTab.options.length)
- option = currentTab.options[index];
- return option;
+ if (index >= currentTab.options.length)
+ return null;
+
+ return currentTab.options[index];
}
}
diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java
index 87704504..4e9cc4e6 100644
--- a/src/itdelatrisu/opsu/states/SongMenu.java
+++ b/src/itdelatrisu/opsu/states/SongMenu.java
@@ -22,25 +22,25 @@ import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
-import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuFile;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.OsuGroupNode;
-import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.ScoreData;
-import itdelatrisu.opsu.SongSort;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MultiClip;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
-import itdelatrisu.opsu.db.OsuDB;
+import itdelatrisu.opsu.beatmap.Beatmap;
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapSetNode;
+import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
+import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
+import itdelatrisu.opsu.ui.MenuButton;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.util.Map;
@@ -91,28 +91,28 @@ public class SongMenu extends BasicGameState {
/** Line width of the header/footer divider. */
private static final int DIVIDER_LINE_WIDTH = 4;
- /** Song node class representing an OsuGroupNode and file index. */
+ /** Song node class representing an BeatmapSetNode and file index. */
private static class SongNode {
/** Song node. */
- private OsuGroupNode node;
+ private BeatmapSetNode node;
/** File index. */
private int index;
/**
* Constructor.
- * @param node the OsuGroupNode
+ * @param node the BeatmapSetNode
* @param index the file index
*/
- public SongNode(OsuGroupNode node, int index) {
+ public SongNode(BeatmapSetNode node, int index) {
this.node = node;
this.index = index;
}
/**
- * Returns the associated OsuGroupNode.
+ * Returns the associated BeatmapSetNode.
*/
- public OsuGroupNode getNode() { return node; }
+ public BeatmapSetNode getNode() { return node; }
/**
* Returns the associated file index.
@@ -121,10 +121,10 @@ public class SongMenu extends BasicGameState {
}
/** Current start node (topmost menu entry). */
- private OsuGroupNode startNode;
+ private BeatmapSetNode startNode;
/** Current focused (selected) node. */
- private OsuGroupNode focusNode;
+ private BeatmapSetNode focusNode;
/** The base node of the previous focus node. */
private SongNode oldFocusNode = null;
@@ -172,7 +172,7 @@ public class SongMenu extends BasicGameState {
private MenuState stateAction;
/** If non-null, the node that stateAction acts upon. */
- private OsuGroupNode stateActionNode;
+ private BeatmapSetNode stateActionNode;
/** If non-null, the score data that stateAction acts upon. */
private ScoreData stateActionScore;
@@ -228,7 +228,7 @@ public class SongMenu extends BasicGameState {
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
// initialize sorts
- for (SongSort sort : SongSort.values())
+ for (BeatmapSortOrder sort : BeatmapSortOrder.values())
sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2);
// initialize score data buttons
@@ -292,13 +292,13 @@ public class SongMenu extends BasicGameState {
// background
if (focusNode != null) {
- OsuFile focusNodeOsu = focusNode.osuFiles.get(focusNode.osuFileIndex);
- if (!focusNodeOsu.drawBG(width, height, 1.0f, true))
+ Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
+ if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
GameImage.PLAYFIELD.getImage().draw();
}
// song buttons
- OsuGroupNode node = startNode;
+ BeatmapSetNode node = startNode;
int songButtonIndex = 0;
if (node != null && node.prev != null) {
node = node.prev;
@@ -339,21 +339,27 @@ public class SongMenu extends BasicGameState {
if (songInfo == null) {
songInfo = focusNode.getInfo();
if (Options.useUnicodeMetadata()) { // load glyphs
- OsuFile osu = focusNode.osuFiles.get(0);
- Utils.loadGlyphs(Utils.FONT_LARGE, osu.titleUnicode, osu.artistUnicode);
+ Beatmap beatmap = focusNode.getBeatmapSet().get(0);
+ Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
}
}
marginX += 5;
- float headerTextY = marginY;
+ float headerTextY = marginY * 0.2f;
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
- headerTextY += Utils.FONT_LARGE.getLineHeight() - 8;
+ headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
- Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], Color.white);
+ float speedModifier = GameMod.getSpeedMultiplier();
+ Color color2 = (speedModifier == 1f) ? Color.white :
+ (speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
+ Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
- Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], Color.white);
+ float multiplier = GameMod.getDifficultyMultiplier();
+ Color color4 = (multiplier == 1f) ? Color.white :
+ (multiplier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
+ Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
}
// score buttons
@@ -382,15 +388,15 @@ public class SongMenu extends BasicGameState {
selectOptionsButton.draw();
// sorting tabs
- SongSort currentSort = SongSort.getSort();
- SongSort hoverSort = null;
- for (SongSort sort : SongSort.values()) {
+ BeatmapSortOrder currentSort = BeatmapSortOrder.getSort();
+ BeatmapSortOrder hoverSort = null;
+ for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
if (sort.contains(mouseX, mouseY)) {
hoverSort = sort;
break;
}
}
- for (SongSort sort : SongSort.VALUES_REVERSED) {
+ for (BeatmapSortOrder sort : BeatmapSortOrder.VALUES_REVERSED) {
if (sort != currentSort)
sort.draw(false, sort == hoverSort);
}
@@ -431,14 +437,14 @@ public class SongMenu extends BasicGameState {
// scroll bar
if (focusNode != null) {
- int focusNodes = focusNode.osuFiles.size();
- int totalNodes = OsuGroupList.get().size() + focusNodes - 1;
+ int focusNodes = focusNode.getBeatmapSet().size();
+ int totalNodes = BeatmapSetList.get().size() + focusNodes - 1;
if (totalNodes > MAX_SONG_BUTTONS) {
int startIndex = startNode.index;
if (startNode.index > focusNode.index)
startIndex += focusNodes;
else if (startNode.index == focusNode.index)
- startIndex += startNode.osuFileIndex;
+ startIndex += startNode.beatmapIndex;
UI.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS,
width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset,
Utils.COLOR_BLACK_ALPHA, Color.white, true);
@@ -495,9 +501,9 @@ public class SongMenu extends BasicGameState {
// store the start/focus nodes
if (focusNode != null)
- oldFocusNode = new SongNode(OsuGroupList.get().getBaseNode(focusNode.index), focusNode.osuFileIndex);
+ oldFocusNode = new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex);
- if (OsuGroupList.get().search(search.getText())) {
+ if (BeatmapSetList.get().search(search.getText())) {
// reset song stack
randomStack = new Stack();
@@ -509,19 +515,19 @@ public class SongMenu extends BasicGameState {
startNode = focusNode = null;
scoreMap = null;
focusScores = null;
- if (OsuGroupList.get().size() > 0) {
- OsuGroupList.get().init();
+ if (BeatmapSetList.get().size() > 0) {
+ BeatmapSetList.get().init();
if (search.getText().isEmpty()) { // cleared search
// use previous start/focus if possible
if (oldFocusNode != null)
setFocus(oldFocusNode.getNode(), oldFocusNode.getIndex(), true, true);
else
- setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else {
- int size = OsuGroupList.get().size();
+ int size = BeatmapSetList.get().size();
searchResultString = String.format("%d match%s found!",
size, (size == 1) ? "" : "es");
- setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
}
oldFocusNode = null;
} else if (!search.getText().isEmpty())
@@ -549,9 +555,9 @@ public class SongMenu extends BasicGameState {
// mouse hover
boolean isHover = false;
if (mouseY > headerY && mouseY < footerY) {
- OsuGroupNode node = startNode;
+ BeatmapSetNode node = startNode;
for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) {
- float cx = (node.index == OsuGroupList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX;
+ float cx = (node.index == BeatmapSetList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX;
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
if (i == hoverIndex) {
@@ -630,15 +636,15 @@ public class SongMenu extends BasicGameState {
return;
// sorting buttons
- for (SongSort sort : SongSort.values()) {
+ for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
if (sort.contains(x, y)) {
- if (sort != SongSort.getSort()) {
- SongSort.setSort(sort);
+ if (sort != BeatmapSortOrder.getSort()) {
+ BeatmapSortOrder.setSort(sort);
SoundController.playSound(SoundEffect.MENUCLICK);
- OsuGroupNode oldFocusBase = OsuGroupList.get().getBaseNode(focusNode.index);
- int oldFocusFileIndex = focusNode.osuFileIndex;
+ BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
+ int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null;
- OsuGroupList.get().init();
+ BeatmapSetList.get().init();
setFocus(oldFocusBase, oldFocusFileIndex, true, true);
}
return;
@@ -647,8 +653,8 @@ public class SongMenu extends BasicGameState {
// song buttons
if (y > headerY && y < footerY) {
- int expandedIndex = OsuGroupList.get().getExpandedIndex();
- OsuGroupNode node = startNode;
+ int expandedIndex = BeatmapSetList.get().getExpandedIndex();
+ BeatmapSetNode node = startNode;
for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) {
// is button at this index clicked?
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
@@ -659,7 +665,7 @@ public class SongMenu extends BasicGameState {
// clicked node is already expanded
if (node.index == expandedIndex) {
- if (node.osuFileIndex == focusNode.osuFileIndex) {
+ if (node.beatmapIndex == focusNode.beatmapIndex) {
// if already focused, load the beatmap
if (button != Input.MOUSE_RIGHT_BUTTON)
startGame();
@@ -724,7 +730,7 @@ public class SongMenu extends BasicGameState {
switch (key) {
case Input.KEY_ESCAPE:
if (reloadThread != null) {
- // beatmap reloading: stop parsing OsuFiles by sending interrupt to OsuParser
+ // beatmap reloading: stop parsing beatmaps by sending interrupt to BeatmapParser
reloadThread.interrupt();
} else if (!search.getText().isEmpty()) {
// clear search text
@@ -755,8 +761,8 @@ public class SongMenu extends BasicGameState {
setFocus(prev.getNode(), prev.getIndex(), true, true);
} else {
// random track, add previous to stack
- randomStack.push(new SongNode(OsuGroupList.get().getBaseNode(focusNode.index), focusNode.osuFileIndex));
- setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex));
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
}
break;
case Input.KEY_F3:
@@ -776,7 +782,7 @@ public class SongMenu extends BasicGameState {
break;
if (input.isKeyDown(Input.KEY_RSHIFT) || input.isKeyDown(Input.KEY_LSHIFT)) {
SoundController.playSound(SoundEffect.MENUHIT);
- MenuState ms = (focusNode.osuFileIndex == -1 || focusNode.osuFiles.size() == 1) ?
+ MenuState ms = (focusNode.beatmapIndex == -1 || focusNode.getBeatmapSet().size() == 1) ?
MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU);
@@ -810,10 +816,10 @@ public class SongMenu extends BasicGameState {
case Input.KEY_RIGHT:
if (focusNode == null)
break;
- OsuGroupNode next = focusNode.next;
+ BeatmapSetNode next = focusNode.next;
if (next != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
- OsuGroupNode oldStartNode = startNode;
+ BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverIndex = hoverIndex;
setFocus(next, 0, false, true);
@@ -826,13 +832,13 @@ public class SongMenu extends BasicGameState {
case Input.KEY_LEFT:
if (focusNode == null)
break;
- OsuGroupNode prev = focusNode.prev;
+ BeatmapSetNode prev = focusNode.prev;
if (prev != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
- OsuGroupNode oldStartNode = startNode;
+ BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverIndex = hoverIndex;
- setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.osuFiles.size() - 1, false, true);
+ setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset;
hoverIndex = oldHoverIndex;
@@ -937,18 +943,19 @@ public class SongMenu extends BasicGameState {
startScore = 0;
beatmapMenuTimer = -1;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
+ songInfo = null;
// reset song stack
randomStack = new Stack();
// set focus node if not set (e.g. theme song playing)
- if (focusNode == null && OsuGroupList.get().size() > 0)
- setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ if (focusNode == null && BeatmapSetList.get().size() > 0)
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
// reset music track
else if (resetTrack) {
MusicController.pause();
- MusicController.playAt(MusicController.getOsuFile().previewTime, true);
+ MusicController.playAt(MusicController.getBeatmap().previewTime, true);
resetTrack = false;
}
@@ -975,7 +982,7 @@ public class SongMenu extends BasicGameState {
// reload scores
if (focusNode != null) {
- scoreMap = ScoreDB.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex));
+ scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
focusScores = getScoreDataForNode(focusNode, true);
}
@@ -986,31 +993,31 @@ public class SongMenu extends BasicGameState {
if (stateAction != null) {
switch (stateAction) {
case BEATMAP: // clear all scores
- if (stateActionNode == null || stateActionNode.osuFileIndex == -1)
+ if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
break;
- OsuFile osu = stateActionNode.osuFiles.get(stateActionNode.osuFileIndex);
- ScoreDB.deleteScore(osu);
+ Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
+ ScoreDB.deleteScore(beatmap);
if (stateActionNode == focusNode) {
focusScores = null;
- scoreMap.remove(osu.version);
+ scoreMap.remove(beatmap.version);
}
break;
case SCORE: // clear single score
if (stateActionScore == null)
break;
ScoreDB.deleteScore(stateActionScore);
- scoreMap = ScoreDB.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex));
+ scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
focusScores = getScoreDataForNode(focusNode, true);
startScore = 0;
break;
case BEATMAP_DELETE_CONFIRM: // delete song group
if (stateActionNode == null)
break;
- OsuGroupNode
- prev = OsuGroupList.get().getBaseNode(stateActionNode.index - 1),
- next = OsuGroupList.get().getBaseNode(stateActionNode.index + 1);
+ BeatmapSetNode
+ prev = BeatmapSetList.get().getBaseNode(stateActionNode.index - 1),
+ next = BeatmapSetList.get().getBaseNode(stateActionNode.index + 1);
int oldIndex = stateActionNode.index, focusNodeIndex = focusNode.index, startNodeIndex = startNode.index;
- OsuGroupList.get().deleteSongGroup(stateActionNode);
+ BeatmapSetList.get().deleteSongGroup(stateActionNode);
if (oldIndex == focusNodeIndex) {
if (prev != null)
setFocus(prev, -1, true, true);
@@ -1039,7 +1046,7 @@ public class SongMenu extends BasicGameState {
if (stateActionNode == null)
break;
int index = stateActionNode.index;
- OsuGroupList.get().deleteSong(stateActionNode);
+ BeatmapSetList.get().deleteSong(stateActionNode);
if (stateActionNode == focusNode) {
if (stateActionNode.prev != null &&
!(stateActionNode.next != null && stateActionNode.next.index == index)) {
@@ -1081,17 +1088,17 @@ public class SongMenu extends BasicGameState {
@Override
public void run() {
// clear the beatmap cache
- OsuDB.clearDatabase();
+ BeatmapDB.clearDatabase();
// invoke unpacker and parser
File beatmapDir = Options.getBeatmapDir();
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
- OsuParser.parseAllFiles(beatmapDir);
+ BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list
- if (OsuGroupList.get().size() > 0) {
- OsuGroupList.get().init();
- setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ if (BeatmapSetList.get().size() > 0) {
+ BeatmapSetList.get().init();
+ setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
@@ -1139,7 +1146,7 @@ public class SongMenu extends BasicGameState {
n++;
shifted = true;
} else if (n > 0 && startNode.next != null &&
- OsuGroupList.get().getNode(startNode, MAX_SONG_BUTTONS) != null) {
+ BeatmapSetList.get().getNode(startNode, MAX_SONG_BUTTONS) != null) {
startNode = startNode.next;
buttonY -= buttonOffset / 4;
if (buttonY < headerY - height * 0.02f)
@@ -1159,69 +1166,69 @@ public class SongMenu extends BasicGameState {
/**
* Sets a new focus node.
* @param node the base node; it will be expanded if it isn't already
- * @param osuFileIndex the OsuFile element to focus; if out of bounds, it will be randomly chosen
+ * @param beatmapIndex the beatmap element to focus; if out of bounds, it will be randomly chosen
* @param changeStartNode if true, startNode will be set to the first node in the group
* @param preview whether to start at the preview time (true) or beginning (false)
* @return the old focus node
*/
- public OsuGroupNode setFocus(OsuGroupNode node, int osuFileIndex, boolean changeStartNode, boolean preview) {
+ public BeatmapSetNode setFocus(BeatmapSetNode node, int beatmapIndex, boolean changeStartNode, boolean preview) {
if (node == null)
return null;
hoverOffset = 0f;
hoverIndex = -1;
songInfo = null;
- OsuGroupNode oldFocus = focusNode;
+ BeatmapSetNode oldFocus = focusNode;
// expand node before focusing it
- int expandedIndex = OsuGroupList.get().getExpandedIndex();
+ int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index != expandedIndex) {
- node = OsuGroupList.get().expand(node.index);
+ node = BeatmapSetList.get().expand(node.index);
// if start node was previously expanded, move it
if (startNode != null && startNode.index == expandedIndex)
- startNode = OsuGroupList.get().getBaseNode(startNode.index);
+ startNode = BeatmapSetList.get().getBaseNode(startNode.index);
}
- // check osuFileIndex bounds
- int length = node.osuFiles.size();
- if (osuFileIndex < 0 || osuFileIndex > length - 1) // set a random index
- osuFileIndex = (int) (Math.random() * length);
+ // check beatmapIndex bounds
+ int length = node.getBeatmapSet().size();
+ if (beatmapIndex < 0 || beatmapIndex > length - 1) // set a random index
+ beatmapIndex = (int) (Math.random() * length);
// change the focus node
- if (changeStartNode || (startNode.index == 0 && startNode.osuFileIndex == -1 && startNode.prev == null))
+ if (changeStartNode || (startNode.index == 0 && startNode.beatmapIndex == -1 && startNode.prev == null))
startNode = node;
- focusNode = OsuGroupList.get().getNode(node, osuFileIndex);
- OsuFile osu = focusNode.osuFiles.get(focusNode.osuFileIndex);
- MusicController.play(osu, false, preview);
+ focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
+ Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
+ MusicController.play(beatmap, false, preview);
// load scores
- scoreMap = ScoreDB.getMapSetScores(osu);
+ scoreMap = ScoreDB.getMapSetScores(beatmap);
focusScores = getScoreDataForNode(focusNode, true);
startScore = 0;
// check startNode bounds
- while (startNode.index >= OsuGroupList.get().size() + length - MAX_SONG_BUTTONS && startNode.prev != null)
+ while (startNode.index >= BeatmapSetList.get().size() + length - MAX_SONG_BUTTONS && startNode.prev != null)
startNode = startNode.prev;
// make sure focusNode is on the screen (TODO: cleanup...)
- int val = focusNode.index + focusNode.osuFileIndex - (startNode.index + MAX_SONG_BUTTONS) + 1;
+ int val = focusNode.index + focusNode.beatmapIndex - (startNode.index + MAX_SONG_BUTTONS) + 1;
if (val > 0) // below screen
changeIndex(val);
else { // above screen
if (focusNode.index == startNode.index) {
- val = focusNode.index + focusNode.osuFileIndex - (startNode.index + startNode.osuFileIndex);
+ val = focusNode.index + focusNode.beatmapIndex - (startNode.index + startNode.beatmapIndex);
if (val < 0)
changeIndex(val);
} else if (startNode.index > focusNode.index) {
- val = focusNode.index - focusNode.osuFiles.size() + focusNode.osuFileIndex - startNode.index + 1;
+ val = focusNode.index - focusNode.getBeatmapSet().size() + focusNode.beatmapIndex - startNode.index + 1;
if (val < 0)
changeIndex(val);
}
}
// if start node is expanded and on group node, move it
- if (startNode.index == focusNode.index && startNode.osuFileIndex == -1)
+ if (startNode.index == focusNode.index && startNode.beatmapIndex == -1)
changeIndex(1);
return oldFocus;
@@ -1248,7 +1255,7 @@ public class SongMenu extends BasicGameState {
* @param menuState the menu state determining the action
* @param node the song node to perform the action on
*/
- public void doStateActionOnLoad(MenuState menuState, OsuGroupNode node) {
+ public void doStateActionOnLoad(MenuState menuState, BeatmapSetNode node) {
doStateActionOnLoad(menuState, node, null);
}
@@ -1267,32 +1274,32 @@ public class SongMenu extends BasicGameState {
* @param node the song node to perform the action on
* @param scoreData the score data to perform the action on
*/
- private void doStateActionOnLoad(MenuState menuState, OsuGroupNode node, ScoreData scoreData) {
+ private void doStateActionOnLoad(MenuState menuState, BeatmapSetNode node, ScoreData scoreData) {
stateAction = menuState;
stateActionNode = node;
stateActionScore = scoreData;
}
/**
- * Returns all the score data for an OsuGroupNode from scoreMap.
+ * Returns all the score data for an BeatmapSetNode from scoreMap.
* If no score data is available for the node, return null.
- * @param node the OsuGroupNode
+ * @param node the BeatmapSetNode
* @param setTimeSince whether or not to set the "time since" field for the scores
* @return the ScoreData array
*/
- private ScoreData[] getScoreDataForNode(OsuGroupNode node, boolean setTimeSince) {
- if (scoreMap == null || scoreMap.isEmpty() || node.osuFileIndex == -1) // node not expanded
+ private ScoreData[] getScoreDataForNode(BeatmapSetNode node, boolean setTimeSince) {
+ if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
return null;
- OsuFile osu = node.osuFiles.get(node.osuFileIndex);
- ScoreData[] scores = scoreMap.get(osu.version);
+ Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
+ ScoreData[] scores = scoreMap.get(beatmap.version);
if (scores == null || scores.length < 1) // no scores
return null;
ScoreData s = scores[0];
- if (osu.beatmapID == s.MID && osu.beatmapSetID == s.MSID &&
- osu.title.equals(s.title) && osu.artist.equals(s.artist) &&
- osu.creator.equals(s.creator)) {
+ if (beatmap.beatmapID == s.MID && beatmap.beatmapSetID == s.MSID &&
+ beatmap.title.equals(s.title) && beatmap.artist.equals(s.artist) &&
+ beatmap.creator.equals(s.creator)) {
if (setTimeSince) {
for (int i = 0; i < scores.length; i++)
scores[i].getTimeSince();
@@ -1311,9 +1318,9 @@ public class SongMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUHIT);
MultiClip.destroyExtraClips();
- OsuFile osu = MusicController.getOsuFile();
+ Beatmap beatmap = MusicController.getBeatmap();
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
- gameState.loadOsuFile(osu);
+ gameState.loadBeatmap(beatmap);
gameState.setRestart(Game.Restart.NEW);
gameState.setReplay(null);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java
index 90ee8fcf..b69c95d3 100644
--- a/src/itdelatrisu/opsu/states/Splash.java
+++ b/src/itdelatrisu/opsu/states/Splash.java
@@ -21,14 +21,15 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
-import itdelatrisu.opsu.OsuGroupList;
-import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker;
-import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.replay.ReplayImporter;
+//conflict
+import itdelatrisu.opsu.beatmap.BeatmapSetList;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
+import itdelatrisu.opsu.ui.UI;
import java.io.File;
@@ -56,6 +57,9 @@ public class Splash extends BasicGameState {
/** Number of times the 'Esc' key has been pressed. */
private int escapeCount = 0;
+ /** Whether the skin being loaded is a new skin (for program restarts). */
+ private boolean newSkin = false;
+
// game-related variables
private int state;
private GameContainer container;
@@ -70,6 +74,10 @@ public class Splash extends BasicGameState {
throws SlickException {
this.container = container;
+ // check if skin changed
+ if (Options.getSkin() != null)
+ this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
+
// load Utils class first (needed in other 'init' methods)
Utils.init(container, game);
@@ -90,11 +98,27 @@ public class Splash extends BasicGameState {
if (!init) {
init = true;
- if (OsuGroupList.get() != null) {
- // resources already loaded (from application restart)
- finished = true;
- } else {
- // load resources in a new thread
+ // resources already loaded (from application restart)
+ if (BeatmapSetList.get() != null) {
+ // reload sounds if skin changed
+ if (newSkin) {
+ thread = new Thread() {
+ @Override
+ public void run() {
+ // TODO: only reload each sound if actually needed?
+ SoundController.init();
+
+ finished = true;
+ thread = null;
+ }
+ };
+ thread.start();
+ } else // don't reload anything
+ finished = true;
+ }
+
+ // load all resources in a new thread
+ else {
thread = new Thread() {
@Override
public void run() {
@@ -104,7 +128,7 @@ public class Splash extends BasicGameState {
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
// parse song directory
- OsuParser.parseAllFiles(beatmapDir);
+ BeatmapParser.parseAllFiles(beatmapDir);
// import replays
ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir());
@@ -129,12 +153,12 @@ public class Splash extends BasicGameState {
// change states when loading complete
if (finished && alpha >= 1f) {
// initialize song list
- if (OsuGroupList.get().size() > 0) {
- OsuGroupList.get().init();
+ if (BeatmapSetList.get().size() > 0) {
+ BeatmapSetList.get().init();
if (Options.isThemeSongEnabled())
MusicController.playThemeSong();
else
- ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(OsuGroupList.get().getRandomNode(), -1, true, true);
+ ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
}
// play the theme song
@@ -155,7 +179,7 @@ public class Splash extends BasicGameState {
if (++escapeCount >= 3)
container.exit();
- // stop parsing OsuFiles by sending interrupt to OsuParser
+ // stop parsing beatmaps by sending interrupt to BeatmapParser
else if (thread != null)
thread.interrupt();
}
diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java
new file mode 100644
index 00000000..d990205c
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/Cursor.java
@@ -0,0 +1,309 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.ui;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Opsu;
+import itdelatrisu.opsu.Options;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.skins.Skin;
+
+import java.nio.IntBuffer;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import org.lwjgl.BufferUtils;
+import org.lwjgl.LWJGLException;
+import org.newdawn.slick.GameContainer;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.Input;
+import org.newdawn.slick.SlickException;
+import org.newdawn.slick.state.StateBasedGame;
+
+/**
+ * Updates and draws the cursor.
+ */
+public class Cursor {
+ /** Empty cursor. */
+ private static org.lwjgl.input.Cursor emptyCursor;
+
+ /** Last cursor coordinates. */
+ private int lastX = -1, lastY = -1;
+
+ /** Cursor rotation angle. */
+ private float cursorAngle = 0f;
+
+ /** Stores all previous cursor locations to display a trail. */
+ private LinkedList cursorX, cursorY;
+
+ // game-related variables
+ private static GameContainer container;
+ private static StateBasedGame game;
+ private static Input input;
+
+ /**
+ * Initializes the class.
+ * @param container the game container
+ * @param game the game object
+ */
+ public static void init(GameContainer container, StateBasedGame game) {
+ Cursor.container = container;
+ Cursor.game = game;
+ Cursor.input = container.getInput();
+
+ // create empty cursor to simulate hiding the cursor
+ try {
+ int min = org.lwjgl.input.Cursor.getMinCursorSize();
+ IntBuffer tmp = BufferUtils.createIntBuffer(min * min);
+ emptyCursor = new org.lwjgl.input.Cursor(min, min, min/2, min/2, 1, tmp, null);
+ } catch (LWJGLException e) {
+ ErrorHandler.error("Failed to create hidden cursor.", e, true);
+ }
+ }
+
+ /**
+ * Constructor.
+ */
+ public Cursor() {
+ cursorX = new LinkedList();
+ cursorY = new LinkedList();
+ }
+
+ /**
+ * Draws the cursor.
+ */
+ public void draw() {
+ int state = game.getCurrentStateID();
+ boolean mousePressed =
+ (((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) ||
+ ((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) &&
+ !(state == Opsu.STATE_GAME && Options.isMouseDisabled())));
+ draw(input.getMouseX(), input.getMouseY(), mousePressed);
+ }
+
+ /**
+ * Draws the cursor.
+ * @param mouseX the mouse x coordinate
+ * @param mouseY the mouse y coordinate
+ * @param mousePressed whether or not the mouse button is pressed
+ */
+ public void draw(int mouseX, int mouseY, boolean mousePressed) {
+ // determine correct cursor image
+ Image cursor = null, cursorMiddle = null, cursorTrail = null;
+ boolean skinned = GameImage.CURSOR.hasSkinImage();
+ boolean newStyle, hasMiddle;
+ if (skinned) {
+ newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
+ hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
+ } else
+ newStyle = hasMiddle = Options.isNewCursorEnabled();
+ if (skinned || newStyle) {
+ cursor = GameImage.CURSOR.getImage();
+ cursorTrail = GameImage.CURSOR_TRAIL.getImage();
+ } else {
+ cursor = GameImage.CURSOR_OLD.getImage();
+ cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
+ }
+ if (hasMiddle)
+ cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
+
+ int removeCount = 0;
+ int FPSmod = (Options.getTargetFPS() / 60);
+ Skin skin = Options.getSkin();
+
+ // TODO: use an image buffer
+ if (newStyle) {
+ // new style: add all points between cursor movements
+ if (lastX < 0) {
+ lastX = mouseX;
+ lastY = mouseY;
+ return;
+ }
+ addCursorPoints(lastX, lastY, mouseX, mouseY);
+ lastX = mouseX;
+ lastY = mouseY;
+
+ removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
+ } else {
+ // old style: sample one point at a time
+ cursorX.add(mouseX);
+ cursorY.add(mouseY);
+
+ int max = 10 * FPSmod;
+ if (cursorX.size() > max)
+ removeCount = cursorX.size() - max;
+ }
+
+ // remove points from the lists
+ for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
+ cursorX.remove();
+ cursorY.remove();
+ }
+
+ // draw a fading trail
+ float alpha = 0f;
+ float t = 2f / cursorX.size();
+ if (skin.isCursorTrailRotated())
+ cursorTrail.setRotation(cursorAngle);
+ Iterator iterX = cursorX.iterator();
+ Iterator iterY = cursorY.iterator();
+ while (iterX.hasNext()) {
+ int cx = iterX.next();
+ int cy = iterY.next();
+ alpha += t;
+ cursorTrail.setAlpha(alpha);
+// if (cx != x || cy != y)
+ cursorTrail.drawCentered(cx, cy);
+ }
+ cursorTrail.drawCentered(mouseX, mouseY);
+
+ // increase the cursor size if pressed
+ if (mousePressed && skin.isCursorExpanded()) {
+ final float scale = 1.25f;
+ cursor = cursor.getScaledCopy(scale);
+ if (hasMiddle)
+ cursorMiddle = cursorMiddle.getScaledCopy(scale);
+ }
+
+ // draw the other components
+ if (newStyle && skin.isCursorRotated())
+ cursor.setRotation(cursorAngle);
+ cursor.drawCentered(mouseX, mouseY);
+ if (hasMiddle)
+ cursorMiddle.drawCentered(mouseX, mouseY);
+ }
+
+ /**
+ * Adds all points between (x1, y1) and (x2, y2) to the cursor point lists.
+ * @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java
+ */
+ private void addCursorPoints(int x1, int y1, int x2, int y2) {
+ // delta of exact value and rounded value of the dependent variable
+ int d = 0;
+ int dy = Math.abs(y2 - y1);
+ int dx = Math.abs(x2 - x1);
+
+ int dy2 = (dy << 1); // slope scaling factors to avoid floating
+ int dx2 = (dx << 1); // point
+ int ix = x1 < x2 ? 1 : -1; // increment direction
+ int iy = y1 < y2 ? 1 : -1;
+
+ int k = 5; // sample size
+ if (dy <= dx) {
+ for (int i = 0; ; i++) {
+ if (i == k) {
+ cursorX.add(x1);
+ cursorY.add(y1);
+ i = 0;
+ }
+ if (x1 == x2)
+ break;
+ x1 += ix;
+ d += dy2;
+ if (d > dx) {
+ y1 += iy;
+ d -= dx2;
+ }
+ }
+ } else {
+ for (int i = 0; ; i++) {
+ if (i == k) {
+ cursorX.add(x1);
+ cursorY.add(y1);
+ i = 0;
+ }
+ if (y1 == y2)
+ break;
+ y1 += iy;
+ d += dx2;
+ if (d > dy) {
+ x1 += ix;
+ d -= dy2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Rotates the cursor by a degree determined by a delta interval.
+ * If the old style cursor is being used, this will do nothing.
+ * @param delta the delta interval since the last call
+ */
+ public void update(int delta) {
+ cursorAngle += delta / 40f;
+ cursorAngle %= 360;
+ }
+
+ /**
+ * Resets all cursor data and skins.
+ */
+ public void reset() {
+ // destroy skin images
+ GameImage.CURSOR.destroySkinImage();
+ GameImage.CURSOR_MIDDLE.destroySkinImage();
+ GameImage.CURSOR_TRAIL.destroySkinImage();
+
+ // reset locations
+ resetLocations();
+
+ // reset angles
+ cursorAngle = 0f;
+ GameImage.CURSOR.getImage().setRotation(0f);
+ GameImage.CURSOR_TRAIL.getImage().setRotation(0f);
+ }
+
+ /**
+ * Resets all cursor location data.
+ */
+ public void resetLocations() {
+ lastX = lastY = -1;
+ cursorX.clear();
+ cursorY.clear();
+ }
+
+ /**
+ * Returns whether or not the cursor is skinned.
+ */
+ public boolean isSkinned() {
+ return (GameImage.CURSOR.hasSkinImage() ||
+ GameImage.CURSOR_MIDDLE.hasSkinImage() ||
+ GameImage.CURSOR_TRAIL.hasSkinImage());
+ }
+
+ /**
+ * Hides the cursor, if possible.
+ */
+ public void hide() {
+ if (emptyCursor != null) {
+ try {
+ container.setMouseCursor(emptyCursor, 0, 0);
+ } catch (SlickException e) {
+ ErrorHandler.error("Failed to hide the cursor.", e, true);
+ }
+ }
+ }
+
+ /**
+ * Unhides the cursor.
+ */
+ public void show() {
+ container.setDefaultMouseCursor();
+ }
+}
diff --git a/src/itdelatrisu/opsu/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java
similarity index 99%
rename from src/itdelatrisu/opsu/MenuButton.java
rename to src/itdelatrisu/opsu/ui/MenuButton.java
index de04a0e3..7370d0bc 100644
--- a/src/itdelatrisu/opsu/MenuButton.java
+++ b/src/itdelatrisu/opsu/ui/MenuButton.java
@@ -16,7 +16,9 @@
* along with opsu!. If not, see .
*/
-package itdelatrisu.opsu;
+package itdelatrisu.opsu.ui;
+
+import itdelatrisu.opsu.Utils;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
@@ -146,7 +148,7 @@ public class MenuButton {
* Sets text to draw in the middle of the button.
* @param text the text to draw
* @param font the font to use when drawing
- * @color the color to draw the text
+ * @param color the color to draw the text
*/
public void setText(String text, Font font, Color color) {
this.text = text;
diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java
new file mode 100644
index 00000000..80e4cd3b
--- /dev/null
+++ b/src/itdelatrisu/opsu/ui/UI.java
@@ -0,0 +1,506 @@
+/*
+ * 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 .
+ */
+
+package itdelatrisu.opsu.ui;
+
+import itdelatrisu.opsu.ErrorHandler;
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Options;
+import itdelatrisu.opsu.OszUnpacker;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.audio.SoundController;
+import itdelatrisu.opsu.beatmap.BeatmapParser;
+
+import javax.swing.JOptionPane;
+import javax.swing.UIManager;
+
+import org.newdawn.slick.Animation;
+import org.newdawn.slick.Color;
+import org.newdawn.slick.GameContainer;
+import org.newdawn.slick.Graphics;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.Input;
+import org.newdawn.slick.SlickException;
+import org.newdawn.slick.state.StateBasedGame;
+
+/**
+ * Draws common UI components.
+ */
+public class UI {
+ /** Cursor. */
+ private static Cursor cursor = new Cursor();
+
+ /** Back button. */
+ private static MenuButton backButton;
+
+ /** Time to show volume image, in milliseconds. */
+ private static final int VOLUME_DISPLAY_TIME = 1500;
+
+ /** Volume display elapsed time. */
+ private static int volumeDisplay = -1;
+
+ /** The current bar notification string. */
+ private static String barNotif;
+
+ /** The current bar notification timer. */
+ private static int barNotifTimer = -1;
+
+ /** Duration, in milliseconds, to display bar notifications. */
+ private static final int BAR_NOTIFICATION_TIME = 1250;
+
+ /** The current tooltip. */
+ private static String tooltip;
+
+ /** Whether or not to check the current tooltip for line breaks. */
+ private static boolean tooltipNewlines;
+
+ /** The current tooltip timer. */
+ private static int tooltipTimer = -1;
+
+ /** Duration, in milliseconds, to fade tooltips. */
+ private static final int TOOLTIP_FADE_TIME = 200;
+
+ // game-related variables
+ private static GameContainer container;
+ private static Input input;
+
+ // This class should not be instantiated.
+ private UI() {}
+
+ /**
+ * Initializes UI data.
+ * @param container the game container
+ * @param game the game object
+ * @throws SlickException
+ */
+ public static void init(GameContainer container, StateBasedGame game)
+ throws SlickException {
+ UI.container = container;
+ UI.input = container.getInput();
+
+ // initialize cursor
+ Cursor.init(container, game);
+ cursor.hide();
+
+ // back button
+ if (GameImage.MENU_BACK.getImages() != null) {
+ Animation back = GameImage.MENU_BACK.getAnimation(120);
+ backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
+ } else {
+ Image back = GameImage.MENU_BACK.getImage();
+ backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
+ }
+ backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
+ }
+
+ /**
+ * Updates all UI components by a delta interval.
+ * @param delta the delta interval since the last call.
+ */
+ public static void update(int delta) {
+ cursor.update(delta);
+ updateVolumeDisplay(delta);
+ updateBarNotification(delta);
+ if (tooltipTimer > 0)
+ tooltipTimer -= delta;
+ }
+
+ /**
+ * Draws the global UI components: cursor, FPS, volume bar, bar notifications.
+ * @param g the graphics context
+ */
+ public static void draw(Graphics g) {
+ drawBarNotification(g);
+ drawVolume(g);
+ drawFPS();
+ cursor.draw();
+ drawTooltip(g);
+ }
+
+ /**
+ * Draws the global UI components: cursor, FPS, volume bar, bar notifications.
+ * @param g the graphics context
+ * @param mouseX the mouse x coordinate
+ * @param mouseY the mouse y coordinate
+ * @param mousePressed whether or not the mouse button is pressed
+ */
+ public static void draw(Graphics g, int mouseX, int mouseY, boolean mousePressed) {
+ drawBarNotification(g);
+ drawVolume(g);
+ drawFPS();
+ cursor.draw(mouseX, mouseY, mousePressed);
+ drawTooltip(g);
+ }
+
+ /**
+ * Resets the necessary UI components upon entering a state.
+ */
+ public static void enter() {
+ backButton.resetHover();
+ cursor.resetLocations();
+ resetBarNotification();
+ resetTooltip();
+ }
+
+ /**
+ * Returns the game cursor.
+ */
+ public static Cursor getCursor() { return cursor; }
+
+ /**
+ * Returns the 'menu-back' MenuButton.
+ */
+ public static MenuButton getBackButton() { return backButton; }
+
+ /**
+ * Draws a tab image and text centered at a location.
+ * @param x the center x coordinate
+ * @param y the center y coordinate
+ * @param text the text to draw inside the tab
+ * @param selected whether the tab is selected (white) or not (red)
+ * @param isHover whether to include a hover effect (unselected only)
+ */
+ public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
+ Image tabImage = GameImage.MENU_TAB.getImage();
+ float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
+ float tabTextY = y - (tabImage.getHeight() / 2);
+ Color filter, textColor;
+ if (selected) {
+ filter = Color.white;
+ textColor = Color.black;
+ } else {
+ filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
+ textColor = Color.white;
+ }
+ tabImage.drawCentered(x, y, filter);
+ Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
+ }
+
+ /**
+ * Draws the FPS at the bottom-right corner of the game container.
+ * If the option is not activated, this will do nothing.
+ */
+ public static void drawFPS() {
+ if (!Options.isFPSCounterEnabled())
+ return;
+
+ String fps = String.format("%dFPS", container.getFPS());
+ Utils.FONT_BOLD.drawString(
+ container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
+ container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
+ Integer.toString(container.getFPS()), Color.white
+ );
+ Utils.FONT_DEFAULT.drawString(
+ container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
+ container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
+ "FPS", Color.white
+ );
+ }
+
+ /**
+ * Draws the volume bar on the middle right-hand side of the game container.
+ * Only draws if the volume has recently been changed using with {@link #changeVolume(int)}.
+ * @param g the graphics context
+ */
+ public static void drawVolume(Graphics g) {
+ if (volumeDisplay == -1)
+ return;
+
+ int width = container.getWidth(), height = container.getHeight();
+ Image img = GameImage.VOLUME.getImage();
+
+ // move image in/out
+ float xOffset = 0;
+ float ratio = (float) volumeDisplay / VOLUME_DISPLAY_TIME;
+ if (ratio <= 0.1f)
+ xOffset = img.getWidth() * (1 - (ratio * 10f));
+ else if (ratio >= 0.9f)
+ xOffset = img.getWidth() * (1 - ((1 - ratio) * 10f));
+
+ img.drawCentered(width - img.getWidth() / 2f + xOffset, height / 2f);
+ float barHeight = img.getHeight() * 0.9f;
+ float volume = Options.getMasterVolume();
+ g.setColor(Color.white);
+ g.fillRoundRect(
+ width - (img.getWidth() * 0.368f) + xOffset,
+ (height / 2f) - (img.getHeight() * 0.47f) + (barHeight * (1 - volume)),
+ img.getWidth() * 0.15f, barHeight * volume, 3
+ );
+ }
+
+ /**
+ * Updates volume display by a delta interval.
+ * @param delta the delta interval since the last call
+ */
+ private static void updateVolumeDisplay(int delta) {
+ if (volumeDisplay == -1)
+ return;
+
+ volumeDisplay += delta;
+ if (volumeDisplay > VOLUME_DISPLAY_TIME)
+ volumeDisplay = -1;
+ }
+
+ /**
+ * Changes the master volume by a unit (positive or negative).
+ * @param units the number of units
+ */
+ public static void changeVolume(int units) {
+ final float UNIT_OFFSET = 0.05f;
+ Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
+ if (volumeDisplay == -1)
+ volumeDisplay = 0;
+ else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
+ volumeDisplay = VOLUME_DISPLAY_TIME / 10;
+ }
+
+ /**
+ * Draws loading progress (OSZ unpacking, beatmap parsing, sound loading)
+ * at the bottom of the screen.
+ */
+ public static void drawLoadingProgress(Graphics g) {
+ String text, file;
+ int progress;
+
+ // determine current action
+ if ((file = OszUnpacker.getCurrentFileName()) != null) {
+ text = "Unpacking new beatmaps...";
+ progress = OszUnpacker.getUnpackerProgress();
+ } else if ((file = BeatmapParser.getCurrentFileName()) != null) {
+ text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ?
+ "Updating database..." : "Loading beatmaps...";
+ progress = BeatmapParser.getParserProgress();
+ } else if ((file = SoundController.getCurrentFileName()) != null) {
+ text = "Loading sounds...";
+ progress = SoundController.getLoadingProgress();
+ } else
+ return;
+
+ // draw loading info
+ float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
+ float lineY = container.getHeight() - marginY;
+ int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
+ if (Options.isLoadVerbose()) {
+ // verbose: display percentages and file names
+ Utils.FONT_MEDIUM.drawString(
+ marginX, lineY - (lineOffsetY * 2),
+ String.format("%s (%d%%)", text, progress), Color.white);
+ Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
+ } else {
+ // draw loading bar
+ Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
+ g.setColor(Color.white);
+ g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
+ (container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
+ );
+ }
+ }
+
+ /**
+ * Draws a scroll bar.
+ * @param g the graphics context
+ * @param unitIndex the unit index
+ * @param totalUnits the total number of units
+ * @param maxShown the maximum number of units shown at one time
+ * @param unitBaseX the base x coordinate of the units
+ * @param unitBaseY the base y coordinate of the units
+ * @param unitWidth the width of a unit
+ * @param unitHeight the height of a unit
+ * @param unitOffsetY the y offset between units
+ * @param bgColor the scroll bar area background color (null if none)
+ * @param scrollbarColor the scroll bar color
+ * @param right whether or not to place the scroll bar on the right side of the unit
+ */
+ public static void drawScrollbar(
+ Graphics g, int unitIndex, int totalUnits, int maxShown,
+ float unitBaseX, float unitBaseY, float unitWidth, float unitHeight, float unitOffsetY,
+ Color bgColor, Color scrollbarColor, boolean right
+ ) {
+ float scrollbarWidth = container.getWidth() * 0.00347f;
+ float heightRatio = (float) (2.6701f * Math.exp(-0.81 * Math.log(totalUnits)));
+ float scrollbarHeight = container.getHeight() * heightRatio;
+ float scrollAreaHeight = unitHeight + unitOffsetY * (maxShown - 1);
+ float offsetY = (scrollAreaHeight - scrollbarHeight) * ((float) unitIndex / (totalUnits - maxShown));
+ float scrollbarX = unitBaseX + unitWidth - ((right) ? scrollbarWidth : 0);
+ if (bgColor != null) {
+ g.setColor(bgColor);
+ g.fillRect(scrollbarX, unitBaseY, scrollbarWidth, scrollAreaHeight);
+ }
+ g.setColor(scrollbarColor);
+ g.fillRect(scrollbarX, unitBaseY + offsetY, scrollbarWidth, scrollbarHeight);
+ }
+
+ /**
+ * Sets or updates a tooltip for drawing.
+ * Must be called with {@link #drawTooltip(Graphics)}.
+ * @param delta the delta interval since the last call
+ * @param s the tooltip text
+ * @param newlines whether to check for line breaks ('\n')
+ */
+ public static void updateTooltip(int delta, String s, boolean newlines) {
+ if (s != null) {
+ tooltip = s;
+ tooltipNewlines = newlines;
+ if (tooltipTimer <= 0)
+ tooltipTimer = delta;
+ else
+ tooltipTimer += delta * 2;
+ if (tooltipTimer > TOOLTIP_FADE_TIME)
+ tooltipTimer = TOOLTIP_FADE_TIME;
+ }
+ }
+
+ /**
+ * Draws a tooltip, if any, near the current mouse coordinates,
+ * bounded by the container dimensions.
+ * @param g the graphics context
+ */
+ public static void drawTooltip(Graphics g) {
+ if (tooltipTimer <= 0 || tooltip == null)
+ return;
+
+ int containerWidth = container.getWidth(), containerHeight = container.getHeight();
+ int margin = containerWidth / 100, textMarginX = 2;
+ int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
+ int lineHeight = Utils.FONT_SMALL.getLineHeight();
+ int textWidth = textMarginX * 2, textHeight = lineHeight;
+ if (tooltipNewlines) {
+ String[] lines = tooltip.split("\\n");
+ int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
+ for (int i = 1; i < lines.length; i++) {
+ int w = Utils.FONT_SMALL.getWidth(lines[i]);
+ if (w > maxWidth)
+ maxWidth = w;
+ }
+ textWidth += maxWidth;
+ textHeight += lineHeight * (lines.length - 1);
+ } else
+ textWidth += Utils.FONT_SMALL.getWidth(tooltip);
+
+ // get drawing coordinates
+ int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
+ if (x + textWidth > containerWidth - margin)
+ x = containerWidth - margin - textWidth;
+ else if (x < margin)
+ x = margin;
+ if (y + textHeight > containerHeight - margin)
+ y = containerHeight - margin - textHeight;
+ else if (y < margin)
+ y = margin;
+
+ // draw tooltip text inside a filled rectangle
+ float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
+ float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
+ Utils.COLOR_BLACK_ALPHA.a = alpha;
+ g.setColor(Utils.COLOR_BLACK_ALPHA);
+ Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
+ g.fillRect(x, y, textWidth, textHeight);
+ oldAlpha = Utils.COLOR_DARK_GRAY.a;
+ Utils.COLOR_DARK_GRAY.a = alpha;
+ g.setColor(Utils.COLOR_DARK_GRAY);
+ g.setLineWidth(1);
+ g.drawRect(x, y, textWidth, textHeight);
+ Utils.COLOR_DARK_GRAY.a = oldAlpha;
+ oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
+ Utils.COLOR_WHITE_ALPHA.a = alpha;
+ Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
+ Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
+ }
+
+ /**
+ * Resets the tooltip.
+ */
+ public static void resetTooltip() {
+ tooltipTimer = -1;
+ tooltip = null;
+ }
+
+ /**
+ * Submits a bar notification for drawing.
+ * Must be called with {@link #drawBarNotification(Graphics)}.
+ * @param s the notification string
+ */
+ public static void sendBarNotification(String s) {
+ if (s != null) {
+ barNotif = s;
+ barNotifTimer = 0;
+ }
+ }
+
+ /**
+ * Updates the bar notification by a delta interval.
+ * @param delta the delta interval since the last call
+ */
+ private static void updateBarNotification(int delta) {
+ if (barNotifTimer > -1 && barNotifTimer < BAR_NOTIFICATION_TIME) {
+ barNotifTimer += delta;
+ if (barNotifTimer > BAR_NOTIFICATION_TIME)
+ barNotifTimer = BAR_NOTIFICATION_TIME;
+ }
+ }
+
+ /**
+ * Resets the bar notification.
+ */
+ public static void resetBarNotification() {
+ barNotifTimer = -1;
+ barNotif = null;
+ }
+
+ /**
+ * Draws the notification sent from {@link #sendBarNotification(String)}.
+ * @param g the graphics context
+ */
+ public static void drawBarNotification(Graphics g) {
+ if (barNotifTimer <= 0 || barNotifTimer >= BAR_NOTIFICATION_TIME)
+ return;
+
+ float alpha = 1f;
+ if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
+ alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
+ int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
+ float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
+ float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
+ Utils.COLOR_BLACK_ALPHA.a *= alpha;
+ Utils.COLOR_WHITE_ALPHA.a = alpha;
+ g.setColor(Utils.COLOR_BLACK_ALPHA);
+ g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
+ Utils.FONT_LARGE.drawString(
+ midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
+ midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
+ barNotif, Utils.COLOR_WHITE_ALPHA);
+ Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
+ Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
+ }
+
+ /**
+ * Shows a confirmation dialog (used before exiting the game).
+ * @param message the message to display
+ * @return true if user selects "yes", false otherwise
+ */
+ public static boolean showExitConfirmation(String message) {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception e) {
+ ErrorHandler.error("Could not set system look and feel for exit confirmation.", e, true);
+ }
+ int n = JOptionPane.showConfirmDialog(null, message, "Warning",
+ JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+ return (n != JOptionPane.YES_OPTION);
+ }
+}
diff --git a/src/org/newdawn/slick/GameContainer.java b/src/org/newdawn/slick/GameContainer.java
index 3c6cff2d..01c7604f 100644
--- a/src/org/newdawn/slick/GameContainer.java
+++ b/src/org/newdawn/slick/GameContainer.java
@@ -555,8 +555,8 @@ public abstract class GameContainer implements GUIContext {
* bottom.
*
* @param ref The reference to the image to be loaded
- * @param x The x-coordinate of the cursor hotspot (left -> right)
- * @param y The y-coordinate of the cursor hotspot (bottom -> top)
+ * @param x The x-coordinate of the cursor hotspot (left {@literal ->} right)
+ * @param y The y-coordinate of the cursor hotspot (bottom {@literal ->} top)
* @param width The x width of the cursor
* @param height The y height of the cursor
* @param cursorDelays image delays between changing frames in animation
diff --git a/src/org/newdawn/slick/Image.java b/src/org/newdawn/slick/Image.java
index 05027b9b..1737a808 100644
--- a/src/org/newdawn/slick/Image.java
+++ b/src/org/newdawn/slick/Image.java
@@ -935,7 +935,7 @@ public class Image implements Renderable {
/**
* Set the angle to rotate this image to. The angle will be normalized to
- * be 0 <= angle < 360. The image will be rotated around its center.
+ * be {@literal 0 <= angle < 360}. The image will be rotated around its center.
*
* @param angle The angle to be set
*/
@@ -973,7 +973,7 @@ public class Image implements Renderable {
/**
* Add the angle provided to the current rotation. The angle will be normalized to
- * be 0 <= angle < 360. The image will be rotated around its center.
+ * be {@literal 0 <= angle < 360}. The image will be rotated around its center.
*
* @param angle The angle to add.
*/
diff --git a/src/org/newdawn/slick/Input.java b/src/org/newdawn/slick/Input.java
new file mode 100644
index 00000000..1066ca34
--- /dev/null
+++ b/src/org/newdawn/slick/Input.java
@@ -0,0 +1,1568 @@
+/*
+ * Copyright (c) 2013, Slick2D
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the Slick2D nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.newdawn.slick;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.lwjgl.LWJGLException;
+import org.lwjgl.input.Controller;
+import org.lwjgl.input.Controllers;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+import org.lwjgl.opengl.Display;
+import org.newdawn.slick.util.Log;
+
+/**
+ * A wrapped for all keyboard, mouse and controller input
+ *
+ * @author kevin
+ */
+@SuppressWarnings({"rawtypes", "unchecked", "unused"})
+public class Input {
+ /** The controller index to pass to check all controllers */
+ public static final int ANY_CONTROLLER = -1;
+
+ /** The maximum number of buttons on controllers */
+ private static final int MAX_BUTTONS = 100;
+
+ /** */
+ public static final int KEY_ESCAPE = 0x01;
+ /** */
+ public static final int KEY_1 = 0x02;
+ /** */
+ public static final int KEY_2 = 0x03;
+ /** */
+ public static final int KEY_3 = 0x04;
+ /** */
+ public static final int KEY_4 = 0x05;
+ /** */
+ public static final int KEY_5 = 0x06;
+ /** */
+ public static final int KEY_6 = 0x07;
+ /** */
+ public static final int KEY_7 = 0x08;
+ /** */
+ public static final int KEY_8 = 0x09;
+ /** */
+ public static final int KEY_9 = 0x0A;
+ /** */
+ public static final int KEY_0 = 0x0B;
+ /** */
+ public static final int KEY_MINUS = 0x0C; /* - on main keyboard */
+ /** */
+ public static final int KEY_EQUALS = 0x0D;
+ /** */
+ public static final int KEY_BACK = 0x0E; /* backspace */
+ /** */
+ public static final int KEY_TAB = 0x0F;
+ /** */
+ public static final int KEY_Q = 0x10;
+ /** */
+ public static final int KEY_W = 0x11;
+ /** */
+ public static final int KEY_E = 0x12;
+ /** */
+ public static final int KEY_R = 0x13;
+ /** */
+ public static final int KEY_T = 0x14;
+ /** */
+ public static final int KEY_Y = 0x15;
+ /** */
+ public static final int KEY_U = 0x16;
+ /** */
+ public static final int KEY_I = 0x17;
+ /** */
+ public static final int KEY_O = 0x18;
+ /** */
+ public static final int KEY_P = 0x19;
+ /** */
+ public static final int KEY_LBRACKET = 0x1A;
+ /** */
+ public static final int KEY_RBRACKET = 0x1B;
+ /** */
+ public static final int KEY_RETURN = 0x1C; /* Enter on main keyboard */
+ /** */
+ public static final int KEY_ENTER = 0x1C; /* Enter on main keyboard */
+ /** */
+ public static final int KEY_LCONTROL = 0x1D;
+ /** */
+ public static final int KEY_A = 0x1E;
+ /** */
+ public static final int KEY_S = 0x1F;
+ /** */
+ public static final int KEY_D = 0x20;
+ /** */
+ public static final int KEY_F = 0x21;
+ /** */
+ public static final int KEY_G = 0x22;
+ /** */
+ public static final int KEY_H = 0x23;
+ /** */
+ public static final int KEY_J = 0x24;
+ /** */
+ public static final int KEY_K = 0x25;
+ /** */
+ public static final int KEY_L = 0x26;
+ /** */
+ public static final int KEY_SEMICOLON = 0x27;
+ /** */
+ public static final int KEY_APOSTROPHE = 0x28;
+ /** */
+ public static final int KEY_GRAVE = 0x29; /* accent grave */
+ /** */
+ public static final int KEY_LSHIFT = 0x2A;
+ /** */
+ public static final int KEY_BACKSLASH = 0x2B;
+ /** */
+ public static final int KEY_Z = 0x2C;
+ /** */
+ public static final int KEY_X = 0x2D;
+ /** */
+ public static final int KEY_C = 0x2E;
+ /** */
+ public static final int KEY_V = 0x2F;
+ /** */
+ public static final int KEY_B = 0x30;
+ /** */
+ public static final int KEY_N = 0x31;
+ /** */
+ public static final int KEY_M = 0x32;
+ /** */
+ public static final int KEY_COMMA = 0x33;
+ /** */
+ public static final int KEY_PERIOD = 0x34; /* . on main keyboard */
+ /** */
+ public static final int KEY_SLASH = 0x35; /* / on main keyboard */
+ /** */
+ public static final int KEY_RSHIFT = 0x36;
+ /** */
+ public static final int KEY_MULTIPLY = 0x37; /* * on numeric keypad */
+ /** */
+ public static final int KEY_LMENU = 0x38; /* left Alt */
+ /** */
+ public static final int KEY_SPACE = 0x39;
+ /** */
+ public static final int KEY_CAPITAL = 0x3A;
+ /** */
+ public static final int KEY_F1 = 0x3B;
+ /** */
+ public static final int KEY_F2 = 0x3C;
+ /** */
+ public static final int KEY_F3 = 0x3D;
+ /** */
+ public static final int KEY_F4 = 0x3E;
+ /** */
+ public static final int KEY_F5 = 0x3F;
+ /** */
+ public static final int KEY_F6 = 0x40;
+ /** */
+ public static final int KEY_F7 = 0x41;
+ /** */
+ public static final int KEY_F8 = 0x42;
+ /** */
+ public static final int KEY_F9 = 0x43;
+ /** */
+ public static final int KEY_F10 = 0x44;
+ /** */
+ public static final int KEY_NUMLOCK = 0x45;
+ /** */
+ public static final int KEY_SCROLL = 0x46; /* Scroll Lock */
+ /** */
+ public static final int KEY_NUMPAD7 = 0x47;
+ /** */
+ public static final int KEY_NUMPAD8 = 0x48;
+ /** */
+ public static final int KEY_NUMPAD9 = 0x49;
+ /** */
+ public static final int KEY_SUBTRACT = 0x4A; /* - on numeric keypad */
+ /** */
+ public static final int KEY_NUMPAD4 = 0x4B;
+ /** */
+ public static final int KEY_NUMPAD5 = 0x4C;
+ /** */
+ public static final int KEY_NUMPAD6 = 0x4D;
+ /** */
+ public static final int KEY_ADD = 0x4E; /* + on numeric keypad */
+ /** */
+ public static final int KEY_NUMPAD1 = 0x4F;
+ /** */
+ public static final int KEY_NUMPAD2 = 0x50;
+ /** */
+ public static final int KEY_NUMPAD3 = 0x51;
+ /** */
+ public static final int KEY_NUMPAD0 = 0x52;
+ /** */
+ public static final int KEY_DECIMAL = 0x53; /* . on numeric keypad */
+ /** */
+ public static final int KEY_F11 = 0x57;
+ /** */
+ public static final int KEY_F12 = 0x58;
+ /** */
+ public static final int KEY_F13 = 0x64; /* (NEC PC98) */
+ /** */
+ public static final int KEY_F14 = 0x65; /* (NEC PC98) */
+ /** */
+ public static final int KEY_F15 = 0x66; /* (NEC PC98) */
+ /** */
+ public static final int KEY_KANA = 0x70; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_CONVERT = 0x79; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_NOCONVERT = 0x7B; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_YEN = 0x7D; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_NUMPADEQUALS = 0x8D; /* = on numeric keypad (NEC PC98) */
+ /** */
+ public static final int KEY_CIRCUMFLEX = 0x90; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_AT = 0x91; /* (NEC PC98) */
+ /** */
+ public static final int KEY_COLON = 0x92; /* (NEC PC98) */
+ /** */
+ public static final int KEY_UNDERLINE = 0x93; /* (NEC PC98) */
+ /** */
+ public static final int KEY_KANJI = 0x94; /* (Japanese keyboard) */
+ /** */
+ public static final int KEY_STOP = 0x95; /* (NEC PC98) */
+ /** */
+ public static final int KEY_AX = 0x96; /* (Japan AX) */
+ /** */
+ public static final int KEY_UNLABELED = 0x97; /* (J3100) */
+ /** */
+ public static final int KEY_NUMPADENTER = 0x9C; /* Enter on numeric keypad */
+ /** */
+ public static final int KEY_RCONTROL = 0x9D;
+ /** */
+ public static final int KEY_NUMPADCOMMA = 0xB3; /* , on numeric keypad (NEC PC98) */
+ /** */
+ public static final int KEY_DIVIDE = 0xB5; /* / on numeric keypad */
+ /** */
+ public static final int KEY_SYSRQ = 0xB7;
+ /** */
+ public static final int KEY_RMENU = 0xB8; /* right Alt */
+ /** */
+ public static final int KEY_PAUSE = 0xC5; /* Pause */
+ /** */
+ public static final int KEY_HOME = 0xC7; /* Home on arrow keypad */
+ /** */
+ public static final int KEY_UP = 0xC8; /* UpArrow on arrow keypad */
+ /** */
+ public static final int KEY_PRIOR = 0xC9; /* PgUp on arrow keypad */
+ /** */
+ public static final int KEY_LEFT = 0xCB; /* LeftArrow on arrow keypad */
+ /** */
+ public static final int KEY_RIGHT = 0xCD; /* RightArrow on arrow keypad */
+ /** */
+ public static final int KEY_END = 0xCF; /* End on arrow keypad */
+ /** */
+ public static final int KEY_DOWN = 0xD0; /* DownArrow on arrow keypad */
+ /** */
+ public static final int KEY_NEXT = 0xD1; /* PgDn on arrow keypad */
+ /** */
+ public static final int KEY_INSERT = 0xD2; /* Insert on arrow keypad */
+ /** */
+ public static final int KEY_DELETE = 0xD3; /* Delete on arrow keypad */
+ /** */
+ public static final int KEY_LWIN = 0xDB; /* Left Windows key */
+ /** */
+ public static final int KEY_RWIN = 0xDC; /* Right Windows key */
+ /** */
+ public static final int KEY_APPS = 0xDD; /* AppMenu key */
+ /** */
+ public static final int KEY_POWER = 0xDE;
+ /** */
+ public static final int KEY_SLEEP = 0xDF;
+
+ /** A helper for left ALT */
+ public static final int KEY_LALT = KEY_LMENU;
+ /** A helper for right ALT */
+ public static final int KEY_RALT = KEY_RMENU;
+
+ /** Control index */
+ private static final int LEFT = 0;
+ /** Control index */
+ private static final int RIGHT = 1;
+ /** Control index */
+ private static final int UP = 2;
+ /** Control index */
+ private static final int DOWN = 3;
+ /** Control index */
+ private static final int BUTTON1 = 4;
+ /** Control index */
+ private static final int BUTTON2 = 5;
+ /** Control index */
+ private static final int BUTTON3 = 6;
+ /** Control index */
+ private static final int BUTTON4 = 7;
+ /** Control index */
+ private static final int BUTTON5 = 8;
+ /** Control index */
+ private static final int BUTTON6 = 9;
+ /** Control index */
+ private static final int BUTTON7 = 10;
+ /** Control index */
+ private static final int BUTTON8 = 11;
+ /** Control index */
+ private static final int BUTTON9 = 12;
+ /** Control index */
+ private static final int BUTTON10 = 13;
+
+ /** The left mouse button indicator */
+ public static final int MOUSE_LEFT_BUTTON = 0;
+ /** The right mouse button indicator */
+ public static final int MOUSE_RIGHT_BUTTON = 1;
+ /** The middle mouse button indicator */
+ public static final int MOUSE_MIDDLE_BUTTON = 2;
+
+ /** True if the controllers system has been initialised */
+ private static boolean controllersInited = false;
+ /** The list of controllers */
+ private static ArrayList controllers = new ArrayList();
+
+ /** The last recorded mouse x position */
+ private int lastMouseX;
+ /** The last recorded mouse y position */
+ private int lastMouseY;
+ /** THe state of the mouse buttons */
+ protected boolean[] mousePressed = new boolean[10];
+ /** THe state of the controller buttons */
+ private boolean[][] controllerPressed = new boolean[100][MAX_BUTTONS];
+
+ /** The character values representing the pressed keys */
+ protected char[] keys = new char[1024];
+ /** True if the key has been pressed since last queries */
+ protected boolean[] pressed = new boolean[1024];
+ /** The time since the next key repeat to be fired for the key */
+ protected long[] nextRepeat = new long[1024];
+
+ /** The control states from the controllers */
+ private boolean[][] controls = new boolean[10][MAX_BUTTONS+10];
+ /** True if the event has been consumed */
+ protected boolean consumed = false;
+ /** A list of listeners to be notified of input events */
+ protected HashSet allListeners = new HashSet();
+ /** The listeners to notify of key events */
+ protected ArrayList keyListeners = new ArrayList();
+ /** The listener to add */
+ protected ArrayList keyListenersToAdd = new ArrayList();
+ /** The listeners to notify of mouse events */
+ protected ArrayList mouseListeners = new ArrayList();
+ /** The listener to add */
+ protected ArrayList mouseListenersToAdd = new ArrayList();
+ /** The listener to nofiy of controller events */
+ protected ArrayList controllerListeners = new ArrayList();
+ /** The current value of the wheel */
+ private int wheel;
+ /** The height of the display */
+ private int height;
+
+ /** True if the display is active */
+ private boolean displayActive = true;
+
+ /** True if key repeat is enabled */
+ private boolean keyRepeat;
+ /** The initial delay for key repeat starts */
+ private int keyRepeatInitial;
+ /** The interval of key repeat */
+ private int keyRepeatInterval;
+
+ /** True if the input is currently paused */
+ private boolean paused;
+ /** The scale to apply to screen coordinates */
+ private float scaleX = 1;
+ /** The scale to apply to screen coordinates */
+ private float scaleY = 1;
+ /** The offset to apply to screen coordinates */
+ private float xoffset = 0;
+ /** The offset to apply to screen coordinates */
+ private float yoffset = 0;
+
+ /** The delay before determining a single or double click */
+ private int doubleClickDelay = 250;
+ /** The timer running out for a single click */
+ private long doubleClickTimeout = 0;
+
+ /** The clicked x position */
+ private int clickX;
+ /** The clicked y position */
+ private int clickY;
+ /** The clicked button */
+ private int clickButton;
+
+ /** The x position location the mouse was pressed */
+ private int pressedX = -1;
+
+ /** The x position location the mouse was pressed */
+ private int pressedY = -1;
+
+ /** The pixel distance the mouse can move to accept a mouse click */
+ private int mouseClickTolerance = 5;
+
+ /**
+ * Disables support for controllers. This means the jinput JAR and native libs
+ * are not required.
+ */
+ public static void disableControllers() {
+ controllersInited = true;
+ }
+
+ /**
+ * Create a new input with the height of the screen
+ *
+ * @param height The height of the screen
+ */
+ public Input(int height) {
+ init(height);
+ }
+
+ /**
+ * Set the double click interval, the time between the first
+ * and second clicks that should be interpreted as a
+ * double click.
+ *
+ * @param delay The delay between clicks
+ */
+ public void setDoubleClickInterval(int delay) {
+ doubleClickDelay = delay;
+ }
+
+ /**
+ * Set the pixel distance the mouse can move to accept a mouse click.
+ * Default is 5.
+ *
+ * @param mouseClickTolerance The number of pixels.
+ */
+ public void setMouseClickTolerance (int mouseClickTolerance) {
+ this.mouseClickTolerance = mouseClickTolerance;
+ }
+
+ /**
+ * Set the scaling to apply to screen coordinates
+ *
+ * @param scaleX The scaling to apply to the horizontal axis
+ * @param scaleY The scaling to apply to the vertical axis
+ */
+ public void setScale(float scaleX, float scaleY) {
+ this.scaleX = scaleX;
+ this.scaleY = scaleY;
+ }
+
+ /**
+ * Set the offset to apply to the screen coodinates
+ *
+ * @param xoffset The offset on the x-axis
+ * @param yoffset The offset on the y-axis
+ */
+ public void setOffset(float xoffset, float yoffset) {
+ this.xoffset = xoffset;
+ this.yoffset = yoffset;
+ }
+
+ /**
+ * Reset the transformation being applied to the input to the default
+ */
+ public void resetInputTransform() {
+ setOffset(0, 0);
+ setScale(1, 1);
+ }
+
+ /**
+ * Add a listener to be notified of input events
+ *
+ * @param listener The listener to be notified
+ */
+ public void addListener(InputListener listener) {
+ addKeyListener(listener);
+ addMouseListener(listener);
+ addControllerListener(listener);
+ }
+
+ /**
+ * Add a key listener to be notified of key input events
+ *
+ * @param listener The listener to be notified
+ */
+ public void addKeyListener(KeyListener listener) {
+ keyListenersToAdd.add(listener);
+ }
+
+ /**
+ * Add a key listener to be notified of key input events
+ *
+ * @param listener The listener to be notified
+ */
+ private void addKeyListenerImpl(KeyListener listener) {
+ if (keyListeners.contains(listener)) {
+ return;
+ }
+ keyListeners.add(listener);
+ allListeners.add(listener);
+ }
+
+ /**
+ * Add a mouse listener to be notified of mouse input events
+ *
+ * @param listener The listener to be notified
+ */
+ public void addMouseListener(MouseListener listener) {
+ mouseListenersToAdd.add(listener);
+ }
+
+ /**
+ * Add a mouse listener to be notified of mouse input events
+ *
+ * @param listener The listener to be notified
+ */
+ private void addMouseListenerImpl(MouseListener listener) {
+ if (mouseListeners.contains(listener)) {
+ return;
+ }
+ mouseListeners.add(listener);
+ allListeners.add(listener);
+ }
+
+ /**
+ * Add a controller listener to be notified of controller input events
+ *
+ * @param listener The listener to be notified
+ */
+ public void addControllerListener(ControllerListener listener) {
+ if (controllerListeners.contains(listener)) {
+ return;
+ }
+ controllerListeners.add(listener);
+ allListeners.add(listener);
+ }
+
+ /**
+ * Remove all the listeners from this input
+ */
+ public void removeAllListeners() {
+ removeAllKeyListeners();
+ removeAllMouseListeners();
+ removeAllControllerListeners();
+ }
+
+ /**
+ * Remove all the key listeners from this input
+ */
+ public void removeAllKeyListeners() {
+ allListeners.removeAll(keyListeners);
+ keyListeners.clear();
+ }
+
+ /**
+ * Remove all the mouse listeners from this input
+ */
+ public void removeAllMouseListeners() {
+ allListeners.removeAll(mouseListeners);
+ mouseListeners.clear();
+ }
+
+ /**
+ * Remove all the controller listeners from this input
+ */
+ public void removeAllControllerListeners() {
+ allListeners.removeAll(controllerListeners);
+ controllerListeners.clear();
+ }
+
+ /**
+ * Add a listener to be notified of input events. This listener
+ * will get events before others that are currently registered
+ *
+ * @param listener The listener to be notified
+ */
+ public void addPrimaryListener(InputListener listener) {
+ removeListener(listener);
+
+ keyListeners.add(0, listener);
+ mouseListeners.add(0, listener);
+ controllerListeners.add(0, listener);
+
+ allListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener that will no longer be notified
+ *
+ * @param listener The listen to be removed
+ */
+ public void removeListener(InputListener listener) {
+ removeKeyListener(listener);
+ removeMouseListener(listener);
+ removeControllerListener(listener);
+ }
+
+ /**
+ * Remove a key listener that will no longer be notified
+ *
+ * @param listener The listen to be removed
+ */
+ public void removeKeyListener(KeyListener listener) {
+ keyListeners.remove(listener);
+
+ if (!mouseListeners.contains(listener) && !controllerListeners.contains(listener)) {
+ allListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Remove a controller listener that will no longer be notified
+ *
+ * @param listener The listen to be removed
+ */
+ public void removeControllerListener(ControllerListener listener) {
+ controllerListeners.remove(listener);
+
+ if (!mouseListeners.contains(listener) && !keyListeners.contains(listener)) {
+ allListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Remove a mouse listener that will no longer be notified
+ *
+ * @param listener The listen to be removed
+ */
+ public void removeMouseListener(MouseListener listener) {
+ mouseListeners.remove(listener);
+
+ if (!controllerListeners.contains(listener) && !keyListeners.contains(listener)) {
+ allListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Initialise the input system
+ *
+ * @param height The height of the window
+ */
+ void init(int height) {
+ this.height = height;
+ lastMouseX = getMouseX();
+ lastMouseY = getMouseY();
+ }
+
+ /**
+ * Get the character representation of the key identified by the specified code
+ *
+ * @param code The key code of the key to retrieve the name of
+ * @return The name or character representation of the key requested
+ */
+ public static String getKeyName(int code) {
+ return Keyboard.getKeyName(code);
+ }
+
+ /**
+ * Check if a particular key has been pressed since this method
+ * was last called for the specified key
+ *
+ * @param code The key code of the key to check
+ * @return True if the key has been pressed
+ */
+ public boolean isKeyPressed(int code) {
+ if (pressed[code]) {
+ pressed[code] = false;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if a mouse button has been pressed since last call
+ *
+ * @param button The button to check
+ * @return True if the button has been pressed since last call
+ */
+ public boolean isMousePressed(int button) {
+ if (mousePressed[button]) {
+ mousePressed[button] = false;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if a controller button has been pressed since last
+ * time
+ *
+ * @param button The button to check for (note that this includes directional controls first)
+ * @return True if the button has been pressed since last time
+ */
+ public boolean isControlPressed(int button) {
+ return isControlPressed(button, 0);
+ }
+
+ /**
+ * Check if a controller button has been pressed since last
+ * time
+ *
+ * @param controller The index of the controller to check
+ * @param button The button to check for (note that this includes directional controls first)
+ * @return True if the button has been pressed since last time
+ */
+ public boolean isControlPressed(int button, int controller) {
+ if (controllerPressed[controller][button]) {
+ controllerPressed[controller][button] = false;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Clear the state for isControlPressed method. This will reset all
+ * controls to not pressed
+ */
+ public void clearControlPressedRecord() {
+ for (int i=0;iisKeyPressed method. This will
+ * resort in all keys returning that they haven't been pressed, until
+ * they are pressed again
+ */
+ public void clearKeyPressedRecord() {
+ Arrays.fill(pressed, false);
+ }
+
+ /**
+ * Clear the state for the isMousePressed method. This will
+ * resort in all mouse buttons returning that they haven't been pressed, until
+ * they are pressed again
+ */
+ public void clearMousePressedRecord() {
+ Arrays.fill(mousePressed, false);
+ }
+
+ /**
+ * Check if a particular key is down
+ *
+ * @param code The key code of the key to check
+ * @return True if the key is down
+ */
+ public boolean isKeyDown(int code) {
+ return Keyboard.isKeyDown(code);
+ }
+
+ /**
+ * Get the absolute x position of the mouse cursor within the container
+ *
+ * @return The absolute x position of the mouse cursor
+ */
+ public int getAbsoluteMouseX() {
+ return Mouse.getX();
+ }
+
+ /**
+ * Get the absolute y position of the mouse cursor within the container
+ *
+ * @return The absolute y position of the mouse cursor
+ */
+ public int getAbsoluteMouseY() {
+ return height - Mouse.getY();
+ }
+
+ /**
+ * Get the x position of the mouse cursor
+ *
+ * @return The x position of the mouse cursor
+ */
+ public int getMouseX() {
+ return (int) ((Mouse.getX() * scaleX)+xoffset);
+ }
+
+ /**
+ * Get the y position of the mouse cursor
+ *
+ * @return The y position of the mouse cursor
+ */
+ public int getMouseY() {
+ return (int) (((height-Mouse.getY()) * scaleY)+yoffset);
+ }
+
+ /**
+ * Check if a given mouse button is down
+ *
+ * @param button The index of the button to check (starting at 0)
+ * @return True if the mouse button is down
+ */
+ public boolean isMouseButtonDown(int button) {
+ return Mouse.isButtonDown(button);
+ }
+
+ /**
+ * Check if any mouse button is down
+ *
+ * @return True if any mouse button is down
+ */
+ private boolean anyMouseDown() {
+ for (int i=0;i<3;i++) {
+ if (Mouse.isButtonDown(i)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a count of the number of controlles available
+ *
+ * @return The number of controllers available
+ */
+ public int getControllerCount() {
+ try {
+ initControllers();
+ } catch (SlickException e) {
+ throw new RuntimeException("Failed to initialise controllers");
+ }
+
+ return controllers.size();
+ }
+
+ /**
+ * Get the number of axis that are avaiable on a given controller
+ *
+ * @param controller The index of the controller to check
+ * @return The number of axis available on the controller
+ */
+ public int getAxisCount(int controller) {
+ return ((Controller) controllers.get(controller)).getAxisCount();
+ }
+
+ /**
+ * Get the value of the axis with the given index
+ *
+ * @param controller The index of the controller to check
+ * @param axis The index of the axis to read
+ * @return The axis value at time of reading
+ */
+ public float getAxisValue(int controller, int axis) {
+ return ((Controller) controllers.get(controller)).getAxisValue(axis);
+ }
+
+ /**
+ * Get the name of the axis with the given index
+ *
+ * @param controller The index of the controller to check
+ * @param axis The index of the axis to read
+ * @return The name of the specified axis
+ */
+ public String getAxisName(int controller, int axis) {
+ return ((Controller) controllers.get(controller)).getAxisName(axis);
+ }
+
+ /**
+ * Check if the controller has the left direction pressed
+ *
+ * @param controller The index of the controller to check
+ * @return True if the controller is pressed to the left
+ */
+ public boolean isControllerLeft(int controller) {
+ if (controller >= getControllerCount()) {
+ return false;
+ }
+
+ if (controller == ANY_CONTROLLER) {
+ for (int i=0;i= getControllerCount()) {
+ return false;
+ }
+
+ if (controller == ANY_CONTROLLER) {
+ for (int i=0;i 0.5f
+ || ((Controller) controllers.get(controller)).getPovX() > 0.5f;
+ }
+
+ /**
+ * Check if the controller has the up direction pressed
+ *
+ * @param controller The index of the controller to check
+ * @return True if the controller is pressed to the up
+ */
+ public boolean isControllerUp(int controller) {
+ if (controller >= getControllerCount()) {
+ return false;
+ }
+
+ if (controller == ANY_CONTROLLER) {
+ for (int i=0;i= getControllerCount()) {
+ return false;
+ }
+
+ if (controller == ANY_CONTROLLER) {
+ for (int i=0;i 0.5f
+ || ((Controller) controllers.get(controller)).getPovY() > 0.5f;
+
+ }
+
+ /**
+ * Check if controller button is pressed
+ *
+ * @param controller The index of the controller to check
+ * @param index The index of the button to check
+ * @return True if the button is pressed
+ */
+ public boolean isButtonPressed(int index, int controller) {
+ if (controller >= getControllerCount()) {
+ return false;
+ }
+
+ if (controller == ANY_CONTROLLER) {
+ for (int i=0;i= 3) && (controller.getButtonCount() < MAX_BUTTONS)) {
+ controllers.add(controller);
+ }
+ }
+
+ Log.info("Found "+controllers.size()+" controllers");
+ for (int i=0;i doubleClickTimeout) {
+ doubleClickTimeout = 0;
+ }
+ }
+
+ this.height = height;
+
+ Iterator allStarts = allListeners.iterator();
+ while (allStarts.hasNext()) {
+ ControlledInputReciever listener = (ControlledInputReciever) allStarts.next();
+ listener.inputStarted();
+ }
+
+ while (Keyboard.next()) {
+ if (Keyboard.getEventKeyState()) {
+ int eventKey = resolveEventKey(Keyboard.getEventKey(), Keyboard.getEventCharacter());
+
+ keys[eventKey] = Keyboard.getEventCharacter();
+ pressed[eventKey] = true;
+ nextRepeat[eventKey] = System.currentTimeMillis() + keyRepeatInitial;
+
+ consumed = false;
+ for (int i=0;i= 0) {
+ if (Mouse.getEventButtonState()) {
+ consumed = false;
+ mousePressed[Mouse.getEventButton()] = true;
+
+ pressedX = (int) (xoffset + (Mouse.getEventX() * scaleX));
+ pressedY = (int) (yoffset + ((height-Mouse.getEventY()) * scaleY));
+
+ for (int i=0;i nextRepeat[i]) {
+ nextRepeat[i] = System.currentTimeMillis() + keyRepeatInterval;
+ consumed = false;
+ for (int j=0;j= BUTTON1) {
+ return isButtonPressed((index-BUTTON1), controllerIndex);
+ }
+
+ throw new RuntimeException("Unknown control index");
+ }
+
+
+ /**
+ * Pauses the polling and sending of input events.
+ */
+ public void pause() {
+ paused = true;
+
+ // Reset all polling arrays
+ clearKeyPressedRecord();
+ clearMousePressedRecord();
+ clearControlPressedRecord();
+ }
+
+ /**
+ * Resumes the polling and sending of input events.
+ */
+ public void resume() {
+ paused = false;
+ }
+
+ /**
+ * Notify listeners that the mouse button has been clicked
+ *
+ * @param button The button that has been clicked
+ * @param x The location at which the button was clicked
+ * @param y The location at which the button was clicked
+ * @param clickCount The number of times the button was clicked (single or double click)
+ */
+ private void fireMouseClicked(int button, int x, int y, int clickCount) {
+ consumed = false;
+ for (int i=0;i
+ * @author Nathan Sweet {@literal }
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class Music {
diff --git a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
index 8fbe3694..2e737886 100644
--- a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
+++ b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java
@@ -46,7 +46,7 @@ import org.newdawn.slick.util.ResourceLoader;
* as required.
*
* @author Kevin Glass
- * @author Nathan Sweet
+ * @author Nathan Sweet {@literal }
* @author Rockstar play and setPosition cleanup
*/
public class OpenALStreamPlayer {
diff --git a/tools/jarsplice-0.40.jar b/tools/jarsplice-0.40.jar
deleted file mode 100644
index 44fd3a6a..00000000
Binary files a/tools/jarsplice-0.40.jar and /dev/null differ