Merge remote-tracking branch 'org/master' into ReplayTest

Conflicts:
	src/itdelatrisu/opsu/GameData.java
	src/itdelatrisu/opsu/Options.java
	src/itdelatrisu/opsu/OsuFile.java
	src/itdelatrisu/opsu/OsuGroupList.java
	src/itdelatrisu/opsu/OsuHitObject.java
	src/itdelatrisu/opsu/OsuParser.java
	src/itdelatrisu/opsu/UI.java
	src/itdelatrisu/opsu/db/OsuDB.java
	src/itdelatrisu/opsu/objects/Circle.java
	src/itdelatrisu/opsu/objects/HitObject.java
	src/itdelatrisu/opsu/objects/Slider.java
	src/itdelatrisu/opsu/objects/Spinner.java
	src/itdelatrisu/opsu/states/Game.java
	src/itdelatrisu/opsu/states/Splash.java
This commit is contained in:
fd
2015-06-13 20:28:30 -04:00
88 changed files with 9798 additions and 1575 deletions

5
.gitignore vendored
View File

@@ -15,5 +15,10 @@
.classpath .classpath
.project .project
# IntelliJ
.idea/
*.iml
*.iws
Thumbs.db Thumbs.db
/target /target

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>itdelatrisu</groupId> <groupId>itdelatrisu</groupId>
<artifactId>opsu</artifactId> <artifactId>opsu</artifactId>
<version>0.8.0</version> <version>0.9.0</version>
<properties> <properties>
<timestamp>${maven.build.timestamp}</timestamp> <timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format> <maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
@@ -115,6 +115,8 @@
<exclude>org/newdawn/slick/GameContainer.*</exclude> <exclude>org/newdawn/slick/GameContainer.*</exclude>
<exclude>org/newdawn/slick/Image.*</exclude> <exclude>org/newdawn/slick/Image.*</exclude>
<exclude>org/newdawn/slick/Music.*</exclude> <exclude>org/newdawn/slick/Music.*</exclude>
<exclude>org/newdawn/slick/Input.*</exclude>
<exclude>org/newdawn/slick/Input$NullOutputStream.*</exclude>
<exclude>org/newdawn/slick/gui/TextField.*</exclude> <exclude>org/newdawn/slick/gui/TextField.*</exclude>
<exclude>org/newdawn/slick/openal/AudioInputStream*</exclude> <exclude>org/newdawn/slick/openal/AudioInputStream*</exclude>
<exclude>org/newdawn/slick/openal/OpenALStreamPlayer*</exclude> <exclude>org/newdawn/slick/openal/OpenALStreamPlayer*</exclude>

BIN
res/DroidSansFallback.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
res/options-background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
res/playback-double.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
res/playback-half.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
res/playback-normal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
res/slidergradient.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -19,8 +19,11 @@
package itdelatrisu.opsu; package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.ui.UI;
import org.lwjgl.opengl.Display; import org.lwjgl.opengl.Display;
import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.AppGameContainer;
@@ -112,20 +115,23 @@ public class Container extends AppGameContainer {
// save user options // save user options
Options.saveOptions(); Options.saveOptions();
// reset cursor
UI.getCursor().reset();
// destroy images // destroy images
InternalTextureLoader.get().clear(); InternalTextureLoader.get().clear();
// reset image references // reset image references
GameImage.clearReferences(); GameImage.clearReferences();
GameData.Grade.clearReferences(); GameData.Grade.clearReferences();
OsuFile.resetImageCache(); Beatmap.getBackgroundImageCache().clear();
// prevent loading tracks from re-initializing OpenAL // prevent loading tracks from re-initializing OpenAL
MusicController.reset(); MusicController.reset();
// reset OsuGroupList data // reset BeatmapSetList data
if (OsuGroupList.get() != null) if (BeatmapSetList.get() != null)
OsuGroupList.get().reset(); BeatmapSetList.get().reset();
} }
@Override @Override

View File

@@ -22,7 +22,10 @@ import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.replay.ReplayFrame;
@@ -44,15 +47,33 @@ public class GameData {
/** Delta multiplier for steady HP drain. */ /** Delta multiplier for steady HP drain. */
public static final float HP_DRAIN_MULTIPLIER = 1 / 200f; public static final float HP_DRAIN_MULTIPLIER = 1 / 200f;
/** Time, in milliseconds, for a hit result to remain existent. */
public static final int HITRESULT_TIME = 833;
/** Time, in milliseconds, for a hit result to fade. */ /** Time, in milliseconds, for a hit result to fade. */
public static final int HITRESULT_FADE_TIME = 500; public static final int HITRESULT_FADE_TIME = 500;
/** Time, in milliseconds, for a hit circle to fade. */
public static final int HITCIRCLE_FADE_TIME = 300;
/** Duration, in milliseconds, of a combo pop effect. */ /** Duration, in milliseconds, of a combo pop effect. */
private static final int COMBO_POP_TIME = 250; private static final int COMBO_POP_TIME = 250;
/** Time, in milliseconds, for a hit error tick to fade. */ /** Time, in milliseconds, for a hit error tick to fade. */
private static final int HIT_ERROR_FADE_TIME = 5000; private static final int HIT_ERROR_FADE_TIME = 5000;
/** Size of a hit circle at the end of the hit animation. */
private static final float HITCIRCLE_ANIM_SCALE = 1.38f;
/** Size of the hit result text at the end of its animation. */
private static final float HITCIRCLE_TEXT_ANIM_SCALE = 1.28f;
/** Time, in milliseconds, for the hit result text to bounce. */
private static final int HITCIRCLE_TEXT_BOUNCE_TIME = 100;
/** Time, in milliseconds, for the hit result text to fade. */
private static final int HITCIRCLE_TEXT_FADE_TIME = 833;
/** Letter grades. */ /** Letter grades. */
public enum Grade { public enum Grade {
NULL (null, null), NULL (null, null),
@@ -170,7 +191,7 @@ public class GameData {
private int[] hitResultOffset; private int[] hitResultOffset;
/** List of hit result objects associated with hit objects. */ /** List of hit result objects associated with hit objects. */
private LinkedBlockingDeque<OsuHitObjectResult> hitResultList; private LinkedBlockingDeque<HitObjectResult> hitResultList;
/** /**
* Class to store hit error information. * Class to store hit error information.
@@ -205,10 +226,11 @@ public class GameData {
/** List containing recent hit error information. */ /** List containing recent hit error information. */
private LinkedBlockingDeque<HitErrorInfo> hitErrorList; private LinkedBlockingDeque<HitErrorInfo> hitErrorList;
/** /** Hit object types, used for drawing results. */
* Hit result helper class. public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINNER }
*/
private class OsuHitObjectResult { /** Hit result helper class. */
private class HitObjectResult {
/** Object start time. */ /** Object start time. */
public int time; public int time;
@@ -221,12 +243,18 @@ public class GameData {
/** Combo color. */ /** Combo color. */
public Color color; public Color color;
/** Whether the hit object was a spinner. */ /** The type of the hit object. */
public boolean isSpinner; public HitObjectType hitResultType;
/** Alpha level (for fading out). */ /** Alpha level (for fading out). */
public float alpha = 1f; public float alpha = 1f;
/** Slider curve. */
public Curve curve;
/** Whether or not to expand when animating. */
public boolean expand;
/** /**
* Constructor. * Constructor.
* @param time the result's starting track position * @param time the result's starting track position
@@ -234,15 +262,19 @@ public class GameData {
* @param x the center x coordinate * @param x the center x coordinate
* @param y the center y coordinate * @param y the center y coordinate
* @param color the color of the hit object * @param color the color of the hit object
* @param isSpinner whether the hit object was a spinner * @param curve the slider curve (or null if not applicable)
* @param expand whether or not the hit result animation should expand (if applicable)
*/ */
public OsuHitObjectResult(int time, int result, float x, float y, Color color, boolean isSpinner) { public HitObjectResult(int time, int result, float x, float y, Color color,
HitObjectType hitResultType, Curve curve, boolean expand) {
this.time = time; this.time = time;
this.result = result; this.result = result;
this.x = x; this.x = x;
this.y = y; this.y = y;
this.color = color; this.color = color;
this.isSpinner = isSpinner; this.hitResultType = hitResultType;
this.curve = curve;
this.expand = expand;
} }
} }
@@ -312,7 +344,7 @@ public class GameData {
/** /**
* Constructor for score viewing. * Constructor for score viewing.
* This will initialize all parameters and images needed for the * This will initialize all parameters and images needed for the
* {@link #drawRankingElements(Graphics, OsuFile)} method. * {@link #drawRankingElements(Graphics, Beatmap)} method.
* @param s the ScoreData object * @param s the ScoreData object
* @param width container width * @param width container width
* @param height container height * @param height container height
@@ -350,7 +382,13 @@ public class GameData {
health = 100f; health = 100f;
healthDisplay = 100f; healthDisplay = 100f;
hitResultCount = new int[HIT_MAX]; hitResultCount = new int[HIT_MAX];
hitResultList = new LinkedBlockingDeque<OsuHitObjectResult>(); if (hitResultList != null) {
for (HitObjectResult hitResult : hitResultList) {
if (hitResult.curve != null)
hitResult.curve.discardCache();
}
}
hitResultList = new LinkedBlockingDeque<HitObjectResult>();
hitErrorList = new LinkedBlockingDeque<HitErrorInfo>(); hitErrorList = new LinkedBlockingDeque<HitErrorInfo>();
fullObjectCount = 0; fullObjectCount = 0;
combo = 0; combo = 0;
@@ -423,21 +461,37 @@ public class GameData {
} }
/** /**
* Returns a default/score text symbol image for a character. * Returns a default text symbol image for a digit.
* @param i the digit [0-9]
*/ */
public Image getDefaultSymbolImage(int i) { return defaultSymbols[i]; } public Image getDefaultSymbolImage(int i) { return defaultSymbols[i]; }
/**
* Returns a score text symbol image for a character.
* @param c the character [0-9,.%x]
*/
public Image getScoreSymbolImage(char c) { return scoreSymbols.get(c); } public Image getScoreSymbolImage(char c) { return scoreSymbols.get(c); }
/** /**
* Sets or returns the health drain rate. * Sets the health drain rate.
* @param drainRate the new drain rate [0-10]
*/ */
public void setDrainRate(float drainRate) { this.drainRate = drainRate; } public void setDrainRate(float drainRate) { this.drainRate = drainRate; }
/**
* Returns the health drain rate.
*/
public float getDrainRate() { return drainRate; } public float getDrainRate() { return drainRate; }
/** /**
* Sets or returns the difficulty. * Sets the overall difficulty level.
* @param difficulty the new difficulty [0-10]
*/ */
public void setDifficulty(float difficulty) { this.difficulty = difficulty; } public void setDifficulty(float difficulty) { this.difficulty = difficulty; }
/**
* Returns the overall difficulty level.
*/
public float getDifficulty() { return difficulty; } public float getDifficulty() { return difficulty; }
/** /**
@@ -572,8 +626,8 @@ public class GameData {
width - margin, symbolHeight, 0.60f, 1f, true); width - margin, symbolHeight, 0.60f, 1f, true);
// map progress circle // map progress circle
OsuFile osu = MusicController.getOsuFile(); Beatmap beatmap = MusicController.getBeatmap();
int firstObjectTime = osu.objects[0].getTime(); int firstObjectTime = beatmap.objects[0].getTime();
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
float circleDiameter = symbolHeight * 0.60f; float circleDiameter = symbolHeight * 0.60f;
int circleX = (int) (width - margin - ( // max width: "100.00%" int circleX = (int) (width - margin - ( // max width: "100.00%"
@@ -590,7 +644,7 @@ public class GameData {
if (trackPosition > firstObjectTime) { if (trackPosition > firstObjectTime) {
// map progress (white) // map progress (white)
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter, g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (osu.endTime - firstObjectTime)) -90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (beatmap.endTime - firstObjectTime))
); );
} else { } else {
// lead-in time (yellow) // lead-in time (yellow)
@@ -732,9 +786,9 @@ public class GameData {
/** /**
* Draws ranking elements: score, results, ranking, game mods. * Draws ranking elements: score, results, ranking, game mods.
* @param g the graphics context * @param g the graphics context
* @param osu the OsuFile * @param beatmap the beatmap
*/ */
public void drawRankingElements(Graphics g, OsuFile osu) { public void drawRankingElements(Graphics g, Beatmap beatmap) {
// TODO Version 2 skins // TODO Version 2 skins
float rankingHeight = 75; float rankingHeight = 75;
float scoreTextScale = 1.0f; float scoreTextScale = 1.0f;
@@ -813,13 +867,12 @@ public class GameData {
g.setColor(Utils.COLOR_BLACK_ALPHA); g.setColor(Utils.COLOR_BLACK_ALPHA);
g.fillRect(0, 0, width, 100 * uiScale); g.fillRect(0, 0, width, 100 * uiScale);
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0); rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
float c = width * 0.01f; float marginX = width * 0.01f, marginY = height * 0.002f;
Utils.FONT_LARGE.drawString(c, c, Utils.FONT_LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", osu.getArtist(), osu.getTitle(), osu.version), Color.white); String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
Utils.FONT_MEDIUM.drawString(c, c + Utils.FONT_LARGE.getLineHeight() - 6, Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", osu.creator), Color.white); String.format("Beatmap by %s", beatmap.creator), Color.white);
Utils.FONT_MEDIUM.drawString( Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
c, c + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
String.format("Played on %s.", scoreData.getTimeString()), Color.white); String.format("Played on %s.", scoreData.getTimeString()), Color.white);
// mod icons // mod icons
@@ -836,20 +889,15 @@ public class GameData {
/** /**
* Draws stored hit results and removes them from the list as necessary. * Draws stored hit results and removes them from the list as necessary.
* @param trackPosition the current track position * @param trackPosition the current track position (in ms)
*/ */
public void drawHitResults(int trackPosition) { public void drawHitResults(int trackPosition) {
Iterator<OsuHitObjectResult> iter = hitResultList.iterator(); Iterator<HitObjectResult> iter = hitResultList.iterator();
while (iter.hasNext()) { while (iter.hasNext()) {
OsuHitObjectResult hitResult = iter.next(); HitObjectResult hitResult = iter.next();
if (hitResult.time + HITRESULT_FADE_TIME > trackPosition) { if (hitResult.time + HITRESULT_TIME > trackPosition) {
// hit result
hitResults[hitResult.result].setAlpha(hitResult.alpha);
hitResults[hitResult.result].drawCentered(hitResult.x, hitResult.y);
hitResults[hitResult.result].setAlpha(1f);
// spinner // spinner
if (hitResult.isSpinner && hitResult.result != HIT_MISS) { if (hitResult.hitResultType == HitObjectType.SPINNER && hitResult.result != HIT_MISS) {
Image spinnerOsu = GameImage.SPINNER_OSU.getImage(); Image spinnerOsu = GameImage.SPINNER_OSU.getImage();
spinnerOsu.setAlpha(hitResult.alpha); spinnerOsu.setAlpha(hitResult.alpha);
spinnerOsu.drawCentered(width / 2, height / 4); spinnerOsu.drawCentered(width / 2, height / 4);
@@ -859,26 +907,75 @@ public class GameData {
// hit lighting // hit lighting
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) { hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
float scale = 1f + ((trackPosition - hitResult.time) / (float) HITRESULT_FADE_TIME); // TODO: add particle system
Image scaledLighting = GameImage.LIGHTING.getImage().getScaledCopy(scale); Image lighting = GameImage.LIGHTING.getImage();
Image scaledLighting1 = GameImage.LIGHTING1.getImage().getScaledCopy(scale); lighting.setAlpha(hitResult.alpha);
scaledLighting.setAlpha(hitResult.alpha); lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledLighting1.setAlpha(hitResult.alpha); }
scaledLighting.draw(hitResult.x - (scaledLighting.getWidth() / 2f), // hit animation
hitResult.y - (scaledLighting.getHeight() / 2f), hitResult.color); if (hitResult.result != HIT_MISS && (
scaledLighting1.draw(hitResult.x - (scaledLighting1.getWidth() / 2f), hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.y - (scaledLighting1.getHeight() / 2f), hitResult.color); 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); hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
} else } else {
if (hitResult.curve != null)
hitResult.curve.discardCache();
iter.remove(); iter.remove();
}
} }
} }
/** /**
* Changes health by a given percentage, modified by drainRate. * Changes health by a given percentage, modified by drainRate.
* @param percent the health percentage
*/ */
public void changeHealth(float percent) { public void changeHealth(float percent) {
// TODO: drainRate formula // TODO: drainRate formula
@@ -890,7 +987,7 @@ public class GameData {
} }
/** /**
* Returns health percentage. * Returns the current health percentage.
*/ */
public float getHealth() { return health; } public float getHealth() { return health; }
@@ -905,6 +1002,7 @@ public class GameData {
/** /**
* Changes score by a raw value (not affected by other modifiers). * Changes score by a raw value (not affected by other modifiers).
* @param value the score value
*/ */
public void changeScore(int value) { score += value; } public void changeScore(int value) { score += value; }
@@ -914,7 +1012,7 @@ public class GameData {
* @param hit100 the number of 100s * @param hit100 the number of 100s
* @param hit50 the number of 50s * @param hit50 the number of 50s
* @param miss the number of misses * @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) { public static float getScorePercent(int hit300, int hit100, int hit50, int miss) {
float percent = 0; float percent = 0;
@@ -969,7 +1067,7 @@ public class GameData {
/** /**
* Returns letter grade based on score data, * 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() { private Grade getGrade() {
return getGrade( return getGrade(
@@ -1073,10 +1171,14 @@ public class GameData {
// combo bursts (at 30, 60, 100+50x) // combo bursts (at 30, 60, 100+50x)
if (Options.isComboBurstEnabled() && if (Options.isComboBurstEnabled() &&
(combo == 30 || combo == 60 || (combo >= 100 && combo % 50 == 0))) { (combo == 30 || combo == 60 || (combo >= 100 && combo % 50 == 0))) {
if (combo == 30) if (Options.getSkin().isComboBurstRandom())
comboBurstIndex = 0; comboBurstIndex = (int) (Math.random() * comboBurstImages.length);
else else {
comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length; if (combo == 30)
comboBurstIndex = 0;
else
comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
}
comboBurstAlpha = 0.8f; comboBurstAlpha = 0.8f;
if ((comboBurstIndex % 2) == 0) if ((comboBurstIndex % 2) == 0)
comboBurstX = width; comboBurstX = width;
@@ -1105,7 +1207,7 @@ public class GameData {
* @param hitObject the hit object * @param hitObject the hit object
* @param repeat the current repeat number * @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; int hitValue = 0;
switch (result) { switch (result) {
case HIT_SLIDER30: case HIT_SLIDER30:
@@ -1135,7 +1237,7 @@ public class GameData {
if (!Options.isPerfectHitBurstEnabled()) if (!Options.isPerfectHitBurstEnabled())
; // hide perfect hit results ; // hide perfect hit results
else 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, 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:
* <p>
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
* <ul>
* <li><strong>Hit Value:</strong> hit result (50, 100, 300), slider ticks, spinner bonus
* <li><strong>Combo:</strong> combo before this hit - 1 (minimum 0)
* <li><strong>Difficulty:</strong> the beatmap difficulty
* <li><strong>Mod:</strong> mod multipliers
* </ul>
* @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 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 x the x coordinate
* @param y the y coordinate * @param y the y coordinate
* @param color the combo color * @param color the combo color
* @param end true if this is the last hit object in the combo * @param end true if this is the last hit object in the combo
* @param hitObject the hit object * @param hitObject the hit object
* @param repeat the current repeat number (for sliders, or 0 otherwise) * @param 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, private int handleHitResult(int time, int result, float x, float y, Color color,
boolean end, OsuHitObject hitObject, int repeat) { boolean end, HitObject hitObject, int repeat, HitObjectType hitResultType) {
// update health, score, and combo streak based on hit result
int hitValue = 0; int hitValue = 0;
boolean perfectHit = false;
switch (result) { switch (result) {
case HIT_300: case HIT_300:
perfectHit = true;
hitValue = 300; hitValue = 300;
changeHealth(5f); changeHealth(5f);
break; break;
@@ -1183,13 +1303,15 @@ public class GameData {
resetComboStreak(); resetComboStreak();
break; break;
default: default:
return; return HIT_MISS;
} }
if (hitValue > 0) { if (hitValue > 0) {
SoundController.playHitSound( SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat), hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat), hitObject.getSampleSet(repeat),
hitObject.getAdditionSampleSet(repeat)); hitObject.getAdditionSampleSet(repeat));
//TODO merge conflict
/** /**
* https://osu.ppy.sh/wiki/Score * https://osu.ppy.sh/wiki/Score
* [SCORE FORMULA] * [SCORE FORMULA]
@@ -1204,7 +1326,11 @@ public class GameData {
comboMulti += 1; comboMulti += 1;
} }
score += (hitValue + (hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25)); score += (hitValue + (hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25));
// calculate score and increment combo streak
changeScore(getScoreForHit(hitValue));
incrementComboStreak(); incrementComboStreak();
//merge conflict end
} }
hitResultCount[result]++; hitResultCount[result]++;
fullObjectCount++; fullObjectCount++;
@@ -1229,12 +1355,43 @@ public class GameData {
comboEnd = 0; 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 ; // hide perfect hit results
else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
; // "relax" and "autopilot" mods: hide misses ; // "relax" and "autopilot" mods: hide misses
else else {
hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitObject.isSpinner())); 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() { private int getDifficultyMultiplier() {
@@ -1258,21 +1415,21 @@ public class GameData {
* Returns a ScoreData object encapsulating all game data. * Returns a ScoreData object encapsulating all game data.
* If score data already exists, the existing object will be returned * If score data already exists, the existing object will be returned
* (i.e. this will not overwrite existing data). * (i.e. this will not overwrite existing data).
* @param osu the OsuFile * @param beatmap the beatmap
* @return the ScoreData object * @return the ScoreData object
*/ */
public ScoreData getScoreData(OsuFile osu) { public ScoreData getScoreData(Beatmap beatmap) {
if (scoreData != null) if (scoreData != null)
return scoreData; return scoreData;
scoreData = new ScoreData(); scoreData = new ScoreData();
scoreData.timestamp = System.currentTimeMillis() / 1000L; scoreData.timestamp = System.currentTimeMillis() / 1000L;
scoreData.MID = osu.beatmapID; scoreData.MID = beatmap.beatmapID;
scoreData.MSID = osu.beatmapSetID; scoreData.MSID = beatmap.beatmapSetID;
scoreData.title = osu.title; scoreData.title = beatmap.title;
scoreData.artist = osu.artist; scoreData.artist = beatmap.artist;
scoreData.creator = osu.creator; scoreData.creator = beatmap.creator;
scoreData.version = osu.version; scoreData.version = beatmap.version;
scoreData.hit300 = hitResultCount[HIT_300]; scoreData.hit300 = hitResultCount[HIT_300];
scoreData.hit100 = hitResultCount[HIT_100]; scoreData.hit100 = hitResultCount[HIT_100];
scoreData.hit50 = hitResultCount[HIT_50]; scoreData.hit50 = hitResultCount[HIT_50];
@@ -1292,10 +1449,10 @@ public class GameData {
* Returns a Replay object encapsulating all game data. * Returns a Replay object encapsulating all game data.
* If a replay already exists and frames is null, the existing object will be returned. * If a replay already exists and frames is null, the existing object will be returned.
* @param frames the replay frames * @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 * @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) if (replay != null && frames == null)
return replay; return replay;
@@ -1303,9 +1460,9 @@ public class GameData {
return null; return null;
replay = new Replay(); replay = new Replay();
replay.mode = OsuFile.MODE_OSU; replay.mode = Beatmap.MODE_OSU;
replay.version = Updater.get().getBuildDate(); 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.playerName = ""; // TODO
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300]; replay.hit300 = (short) hitResultCount[HIT_300];
@@ -1329,6 +1486,7 @@ public class GameData {
/** /**
* Sets the replay object. * Sets the replay object.
* @param replay the replay
*/ */
public void setReplay(Replay replay) { this.replay = replay; } public void setReplay(Replay replay) { this.replay = replay; }

View File

@@ -110,6 +110,7 @@ public enum GameImage {
APPROACHCIRCLE ("approachcircle", "png"), APPROACHCIRCLE ("approachcircle", "png"),
// Slider // Slider
SLIDER_GRADIENT ("slidergradient", "png"),
SLIDER_BALL ("sliderb", "sliderb%d", "png"), SLIDER_BALL ("sliderb", "sliderb%d", "png"),
SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"), SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"),
REVERSEARROW ("reversearrow", "png"), REVERSEARROW ("reversearrow", "png"),
@@ -202,7 +203,6 @@ public enum GameImage {
SCORE_PERCENT ("score-percent", "png"), SCORE_PERCENT ("score-percent", "png"),
SCORE_X ("score-x", "png"), SCORE_X ("score-x", "png"),
LIGHTING ("lighting", "png"), LIGHTING ("lighting", "png"),
LIGHTING1 ("lighting1", "png"),
// Game Mods // Game Mods
MOD_EASY ("selection-mod-easy", "png", false, false), 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 ("selection-selectoptions", "png", false, false),
SELECTION_OTHER_OPTIONS_OVERLAY ("selection-selectoptions-over", "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 // Non-Game Components
VOLUME ("volume-bg", "png", false, false) { VOLUME ("volume-bg", "png", false, false) {
@Override @Override
@@ -329,6 +334,14 @@ public enum GameImage {
return REPOSITORY.process_sub(img, w, h); 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?) // TODO: ensure this image hasn't been modified (checksum?)
ALPHA_MAP ("alpha", "png", false, false); ALPHA_MAP ("alpha", "png", false, false);
@@ -376,10 +389,13 @@ public enum GameImage {
/** The unscaled container height that uiscale is based on. */ /** The unscaled container height that uiscale is based on. */
private static final int UNSCALED_HEIGHT = 768; private static final int UNSCALED_HEIGHT = 768;
/** Filename suffix for HD images. */
public static final String HD_SUFFIX = "@2x";
/** Image HD/SD suffixes. */ /** Image HD/SD suffixes. */
private static final String[] private static final String[]
SUFFIXES_HD = new String[] { "@2x", "" }, SUFFIXES_HD = new String[] { HD_SUFFIX, "" },
SUFFIXES_SD = new String[] { "" }; SUFFIXES_SD = new String[] { "" };
/** /**
* Initializes the GameImage class with container dimensions. * Initializes the GameImage class with container dimensions.
@@ -550,6 +566,7 @@ public enum GameImage {
/** /**
* Sets the image associated with this resource to another image. * Sets the image associated with this resource to another image.
* The skin image takes priority over the default image. * The skin image takes priority over the default image.
* @param img the image to set
*/ */
public void setImage(Image img) { public void setImage(Image img) {
if (skinImage != null) if (skinImage != null)
@@ -561,6 +578,8 @@ public enum GameImage {
/** /**
* Sets an image associated with this resource to another image. * Sets an image associated with this resource to another image.
* The skin image takes priority over the default 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) { public void setImage(Image img, int index) {
if (skinImages != null) { if (skinImages != null) {
@@ -581,8 +600,9 @@ public enum GameImage {
return; return;
// try to load multiple images // try to load multiple images
File skinDir = Options.getSkin().getDirectory();
if (filenameFormat != null) { if (filenameFormat != null) {
if (((defaultImages = loadImageArray(Options.getSkinDir())) != null) || if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
((defaultImages = loadImageArray(null)) != null)) { ((defaultImages = loadImageArray(null)) != null)) {
process(); process();
return; return;
@@ -590,7 +610,7 @@ public enum GameImage {
} }
// try to load a single image // try to load a single image
if (((defaultImage = loadImageSingle(Options.getSkinDir())) != null) || if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
((defaultImage = loadImageSingle(null)) != null)) { ((defaultImage = loadImageSingle(null)) != null)) {
process(); process();
return; return;
@@ -602,6 +622,7 @@ public enum GameImage {
/** /**
* Sets the associated skin image. * Sets the associated skin image.
* If the path does not contain the image, the default image is used. * 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 * @return true if a new skin image is loaded, false otherwise
*/ */
public boolean setSkinImage(File dir) { public boolean setSkinImage(File dir) {
@@ -632,6 +653,7 @@ public enum GameImage {
/** /**
* Attempts to load multiple Images from the 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 * @return an array of the loaded images, or null if not found
*/ */
private Image[] loadImageArray(File dir) { private Image[] loadImageArray(File dir) {
@@ -649,7 +671,7 @@ public enum GameImage {
// add image to list // add image to list
try { try {
Image img = new Image(name); Image img = new Image(name);
if (suffix.equals("@2x")) if (suffix.equals(HD_SUFFIX))
img = img.getScaledCopy(0.5f); img = img.getScaledCopy(0.5f);
list.add(img); list.add(img);
} catch (SlickException e) { } catch (SlickException e) {
@@ -666,6 +688,7 @@ public enum GameImage {
/** /**
* Attempts to load a single Image from the 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 * @return the loaded image, or null if not found
*/ */
private Image loadImageSingle(File dir) { private Image loadImageSingle(File dir) {
@@ -674,7 +697,7 @@ public enum GameImage {
if (name != null) { if (name != null) {
try { try {
Image img = new Image(name); Image img = new Image(name);
if (suffix.equals("@2x")) if (suffix.equals(HD_SUFFIX))
img = img.getScaledCopy(0.5f); img = img.getScaledCopy(0.5f);
return img; return img;
} catch (SlickException e) { } catch (SlickException e) {

View File

@@ -18,6 +18,8 @@
package itdelatrisu.opsu; package itdelatrisu.opsu;
import itdelatrisu.opsu.ui.MenuButton;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -33,7 +35,7 @@ public enum GameMod {
"Easy", "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required."), "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, NO_FAIL (Category.EASY, 1, GameImage.MOD_NO_FAIL, "NF", 1, Input.KEY_W, 0.5f,
"NoFail", "You can't fail. No matter what."), "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."), "HalfTime", "Less zoom."),
HARD_ROCK (Category.HARD, 0, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f, HARD_ROCK (Category.HARD, 0, GameImage.MOD_HARD_ROCK, "HR", 16, Input.KEY_A, 1.06f,
"HardRock", "Everything just got a bit harder..."), "HardRock", "Everything just got a bit harder..."),
@@ -41,7 +43,7 @@ public enum GameMod {
"SuddenDeath", "Miss a note and fail."), "SuddenDeath", "Miss a note and fail."),
// PERFECT (Category.HARD, 1, GameImage.MOD_PERFECT, "PF", 64, Input.KEY_S, 1f, // PERFECT (Category.HARD, 1, GameImage.MOD_PERFECT, "PF", 64, Input.KEY_S, 1f,
// "Perfect", "SS or quit."), // "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."), "DoubleTime", "Zoooooooooom."),
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f, // NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
// "Nightcore", "uguuuuuuuu"), // "Nightcore", "uguuuuuuuu"),
@@ -173,6 +175,12 @@ public enum GameMod {
/** The last calculated score multiplier, or -1f if it must be recalculated. */ /** The last calculated score multiplier, or -1f if it must be recalculated. */
private static float scoreMultiplier = -1f; 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. * Initializes the game mods.
* @param width the container width * @param width the container width
@@ -198,7 +206,7 @@ public enum GameMod {
mod.active = false; mod.active = false;
} }
scoreMultiplier = -1f; scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
} }
/** /**
@@ -216,6 +224,36 @@ public enum GameMod {
return scoreMultiplier; 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). * 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) * @param state the state (bitwise OR of active mods)
*/ */
public static void loadModState(int state) { public static void loadModState(int state) {
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
for (GameMod mod : GameMod.values()) for (GameMod mod : GameMod.values())
mod.active = ((state & mod.getBit()) > 0); mod.active = ((state & mod.getBit()) > 0);
} }
@@ -352,7 +391,7 @@ public enum GameMod {
return; return;
active = !active; active = !active;
scoreMultiplier = -1f; scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
if (checkInverse) { if (checkInverse) {
if (AUTO.isActive()) { if (AUTO.isActive()) {

View File

@@ -31,6 +31,7 @@ import itdelatrisu.opsu.states.MainMenu;
import itdelatrisu.opsu.states.OptionsMenu; import itdelatrisu.opsu.states.OptionsMenu;
import itdelatrisu.opsu.states.SongMenu; import itdelatrisu.opsu.states.SongMenu;
import itdelatrisu.opsu.states.Splash; import itdelatrisu.opsu.states.Splash;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -45,7 +46,6 @@ import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame; import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition; import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.FadeOutTransition; import org.newdawn.slick.state.transition.FadeOutTransition;
import org.newdawn.slick.util.ClasspathLocation;
import org.newdawn.slick.util.DefaultLogSystem; import org.newdawn.slick.util.DefaultLogSystem;
import org.newdawn.slick.util.FileSystemLocation; import org.newdawn.slick.util.FileSystemLocation;
import org.newdawn.slick.util.Log; import org.newdawn.slick.util.Log;
@@ -128,14 +128,14 @@ public class Opsu extends StateBasedGame {
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath()); System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
// set the resource paths // 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/"))); ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
// initialize databases // initialize databases
DBController.init(); try {
DBController.init();
} catch (UnsatisfiedLinkError e) {
errorAndExit(e, "The databases could not be initialized.");
}
// check if just updated // check if just updated
if (args.length >= 2) if (args.length >= 2)
@@ -176,12 +176,7 @@ public class Opsu extends StateBasedGame {
} }
} }
} catch (SlickException e) { } catch (SlickException e) {
// JARs will not run properly inside directories containing '!' errorAndExit(e, "An error occurred while creating the game container.");
// 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);
} }
} }
@@ -207,7 +202,8 @@ public class Opsu extends StateBasedGame {
} else } else
songMenu.resetTrackOnLoad(); songMenu.resetTrackOnLoad();
} }
UI.resetCursor(); if (UI.getCursor().isSkinned())
UI.getCursor().reset();
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return false; 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);
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
//TODO rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -1,3 +1,5 @@
//TODO rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<OsuFile> 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<OsuFile> 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.
* <ul>
* <li>0: {Artist} - {Title} [{Version}]
* <li>1: Mapped by {Creator}
* <li>2: Length: {} BPM: {} Objects: {}
* <li>3: Circles: {} Sliders: {} Spinners: {}
* <li>4: CS:{} HP:{} AR:{} OD:{}
* </ul>
*/
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;
}
}

View File

@@ -1,3 +1,5 @@
//TODO rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -1,3 +1,5 @@
//TODO rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -20,6 +20,7 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade; import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.states.SongMenu; import itdelatrisu.opsu.states.SongMenu;
import itdelatrisu.opsu.ui.UI;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;

View File

@@ -1,3 +1,5 @@
//TODO rename?
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -20,8 +20,11 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadNode; import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.ui.UI;
import java.awt.Font; import java.awt.Font;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@@ -34,6 +37,7 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -49,6 +53,9 @@ import java.util.Scanner;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display; import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
@@ -78,10 +85,6 @@ public class Utils {
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255), COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
COLOR_BLUE_BUTTON = new Color(40, 129, 237), COLOR_BLUE_BUTTON = new Color(40, 129, 237),
COLOR_ORANGE_BUTTON = new Color(200, 90, 3), 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_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f), COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
COLOR_RED_HOVER = new Color(255, 112, 112), 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_GREEN = new Color(128,255,128),
COLOR_LIGHT_BLUE = new Color(128,128,255), COLOR_LIGHT_BLUE = new Color(128,128,255),
COLOR_GREEN_SEARCH = new Color(173, 255, 47), COLOR_GREEN_SEARCH = new Color(173, 255, 47),
COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f); COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
COLOR_RED_HIGHLIGHT = new Color(246, 154, 161),
/** The default map colors, used when a map does not provide custom colors. */ COLOR_BLUE_HIGHLIGHT = new Color(173, 216, 230);
public static final Color[] DEFAULT_COMBO = {
COLOR_ORANGE_OBJECT, COLOR_GREEN_OBJECT,
COLOR_BLUE_OBJECT, COLOR_RED_OBJECT,
};
/** Game fonts. */ /** Game fonts. */
public static UnicodeFont public static UnicodeFont
@@ -161,16 +160,19 @@ public class Utils {
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2)); FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase)); FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect(); ColorEffect colorEffect = new ColorEffect();
loadFont(FONT_DEFAULT, 2, colorEffect); loadFont(FONT_DEFAULT, colorEffect);
loadFont(FONT_BOLD, 2, colorEffect); loadFont(FONT_BOLD, colorEffect);
loadFont(FONT_XLARGE, 4, colorEffect); loadFont(FONT_XLARGE, colorEffect);
loadFont(FONT_LARGE, 4, colorEffect); loadFont(FONT_LARGE, colorEffect);
loadFont(FONT_MEDIUM, 3, colorEffect); loadFont(FONT_MEDIUM, colorEffect);
loadFont(FONT_SMALL, 1, colorEffect); loadFont(FONT_SMALL, colorEffect);
} catch (Exception e) { } catch (Exception e) {
ErrorHandler.error("Failed to load fonts.", e, true); ErrorHandler.error("Failed to load fonts.", e, true);
} }
// load skin
Options.loadSkin();
// initialize game images // initialize game images
for (GameImage img : GameImage.values()) { for (GameImage img : GameImage.values()) {
if (img.isPreload()) if (img.isPreload())
@@ -180,8 +182,11 @@ public class Utils {
// initialize game mods // initialize game mods
GameMod.init(width, height); GameMod.init(width, height);
// initialize playback buttons
PlaybackSpeed.init(width, height);
// initialize hit objects // initialize hit objects
OsuHitObject.init(width, height); HitObject.init(width, height);
// initialize download nodes // initialize download nodes
DownloadNode.init(width, height); DownloadNode.init(width, height);
@@ -331,15 +336,11 @@ public class Utils {
/** /**
* Loads a Unicode font. * Loads a Unicode font.
* @param font the font to load * @param font the font to load
* @param padding the top and bottom padding
* @param effect the font effect * @param effect the font effect
* @throws SlickException * @throws SlickException
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, int padding, private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
Effect effect) throws SlickException {
font.setPaddingTop(padding);
font.setPaddingBottom(padding);
font.addAsciiGlyphs(); font.addAsciiGlyphs();
font.getEffects().add(effect); font.getEffects().add(effect);
font.loadGlyphs(); font.loadGlyphs();
@@ -516,6 +517,7 @@ public class Utils {
* Returns a the contents of a URL as a string. * Returns a the contents of a URL as a string.
* @param url the remote URL * @param url the remote URL
* @return the contents as a string, or null if any error occurred * @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 { public static String readDataFromUrl(URL url) throws IOException {
// open connection // 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. * Converts an input stream to a string.
* @param is the input stream * @param is the input stream
@@ -601,4 +639,60 @@ public class Utils {
else else
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60); 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);
}
} }

View File

@@ -105,6 +105,12 @@ public enum HitSound implements SoundController.SoundComponent {
return (currentSampleSet != null) ? clips.get(currentSampleSet) : null; 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. * Sets the hit sound Clip for the sample type.
* @param s the sample set * @param s the sample set

View File

@@ -20,8 +20,8 @@ package itdelatrisu.opsu.audio;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -50,8 +50,8 @@ public class MusicController {
/** The current music track. */ /** The current music track. */
private static Music player; private static Music player;
/** The last OsuFile passed to play(). */ /** The last beatmap passed to play(). */
private static OsuFile lastOsu; private static Beatmap lastBeatmap;
/** The track duration. */ /** The track duration. */
private static int duration = 0; private static int duration = 0;
@@ -80,23 +80,23 @@ public class MusicController {
/** /**
* Plays an audio file at the preview position. * Plays an audio file at the preview position.
* If the audio file is already playing, then nothing will happen. * 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 loop whether or not to loop the track
* @param preview whether to start at the preview time (true) or beginning (false) * @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 // new track: load and play
if (lastOsu == null || !osu.audioFilename.equals(lastOsu.audioFilename)) { if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
reset(); reset();
System.gc(); System.gc();
switch (OsuParser.getExtension(osu.audioFilename.getName())) { switch (BeatmapParser.getExtension(beatmap.audioFilename.getName())) {
case "ogg": case "ogg":
case "mp3": case "mp3":
trackLoader = new Thread() { trackLoader = new Thread() {
@Override @Override
public void run() { public void run() {
loadTrack(osu.audioFilename, (preview) ? osu.previewTime : 0, loop); loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
} }
}; };
trackLoader.start(); trackLoader.start();
@@ -107,10 +107,10 @@ public class MusicController {
} }
// new track position: play at position // new track position: play at position
else if (osu.previewTime != lastOsu.previewTime) else if (beatmap.previewTime != lastBeatmap.previewTime)
playAt(osu.previewTime, loop); playAt(beatmap.previewTime, loop);
lastOsu = osu; lastBeatmap = beatmap;
} }
/** /**
@@ -170,9 +170,9 @@ public class MusicController {
public static boolean trackExists() { return (player != null); } 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. * 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. * If no track is loaded, 0 will be returned.
*/ */
public static int getPosition() { public static int getPosition() {
@@ -242,6 +242,7 @@ public class MusicController {
/** /**
* Seeks to a position in the current track. * Seeks to a position in the current track.
* @param position the new track position (in ms)
*/ */
public static boolean setPosition(int position) { public static boolean setPosition(int position) {
return (trackExists() && position >= 0 && player.setPosition(position / 1000f)); return (trackExists() && position >= 0 && player.setPosition(position / 1000f));
@@ -251,17 +252,18 @@ public class MusicController {
* Returns the duration of the current track, in milliseconds. * Returns the duration of the current track, in milliseconds.
* Currently only works for MP3s. * Currently only works for MP3s.
* @return the duration, or -1 if no track exists, else the {@code endTime} * @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) * @author Tom Brito (http://stackoverflow.com/a/3056161)
*/ */
public static int getDuration() { public static int getDuration() {
if (!trackExists() || lastOsu == null) if (!trackExists() || lastBeatmap == null)
return -1; return -1;
if (duration == 0) { if (duration == 0) {
if (lastOsu.audioFilename.getName().endsWith(".mp3")) { // TAudioFileFormat method only works for MP3s
if (lastBeatmap.audioFilename.getName().endsWith(".mp3")) {
try { try {
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(lastOsu.audioFilename); AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(lastBeatmap.audioFilename);
if (fileFormat instanceof TAudioFileFormat) { if (fileFormat instanceof TAudioFileFormat) {
Map<?, ?> properties = ((TAudioFileFormat) fileFormat).properties(); Map<?, ?> properties = ((TAudioFileFormat) fileFormat).properties();
Long microseconds = (Long) properties.get("duration"); Long microseconds = (Long) properties.get("duration");
@@ -270,7 +272,9 @@ public class MusicController {
} }
} catch (UnsupportedAudioFileException | IOException e) {} } catch (UnsupportedAudioFileException | IOException e) {}
} }
duration = lastOsu.endTime;
// fallback: use beatmap end time (often not the track duration)
duration = lastBeatmap.endTime;
} }
return duration; return duration;
} }
@@ -291,12 +295,20 @@ public class MusicController {
/** /**
* Sets the music volume. * Sets the music volume.
* @param volume [0, 1] * @param volume the new volume [0, 1]
*/ */
public static void setVolume(float volume) { public static void setVolume(float volume) {
SoundStore.get().setMusicVolume((isTrackDimmed()) ? volume * dimLevel : 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. * Returns whether or not the current track has ended.
*/ */
@@ -308,16 +320,16 @@ public class MusicController {
*/ */
public static void loopTrackIfEnded(boolean preview) { public static void loopTrackIfEnded(boolean preview) {
if (trackEnded && trackExists()) if (trackEnded && trackExists())
playAt((preview) ? lastOsu.previewTime : 0, false); playAt((preview) ? lastBeatmap.previewTime : 0, false);
} }
/** /**
* Plays the theme song. * Plays the theme song.
*/ */
public static void playThemeSong() { public static void playThemeSong() {
OsuFile osu = Options.getOsuTheme(); Beatmap beatmap = Options.getThemeBeatmap();
if (osu != null) { if (beatmap != null) {
play(osu, true, false); play(beatmap, true, false);
themePlaying = true; themePlaying = true;
} }
} }
@@ -368,7 +380,7 @@ public class MusicController {
trackLoader = null; trackLoader = null;
// reset state // reset state
lastOsu = null; lastBeatmap = null;
duration = 0; duration = 0;
trackEnded = false; trackEnded = false;
themePlaying = false; themePlaying = false;

View File

@@ -20,8 +20,8 @@ package itdelatrisu.opsu.audio;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.audio.HitSound.SampleSet; import itdelatrisu.opsu.audio.HitSound.SampleSet;
import itdelatrisu.opsu.beatmap.HitObject;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -164,7 +164,7 @@ public class SoundController {
} }
if (bestIndex >= 0) if (bestIndex >= 0)
return new MultiClip(ref, AudioSystem.getAudioInputStream(formats[bestIndex], audioIn)); return new MultiClip(ref, AudioSystem.getAudioInputStream(formats[bestIndex], audioIn));
// still couldn't find anything, try the default clip format // still couldn't find anything, try the default clip format
return new MultiClip(ref, AudioSystem.getAudioInputStream(clip.getFormat(), audioIn)); return new MultiClip(ref, AudioSystem.getAudioInputStream(clip.getFormat(), audioIn));
} }
@@ -177,11 +177,14 @@ public class SoundController {
*/ */
private static String getSoundFileName(String filename) { private static String getSoundFileName(String filename) {
String wav = String.format("%s.wav", filename), mp3 = String.format("%s.mp3", 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); File skinDir = Options.getSkin().getDirectory();
if (skinWAV.isFile()) if (skinDir != null) {
return skinWAV.getAbsolutePath(); File skinWAV = new File(skinDir, wav), skinMP3 = new File(skinDir, mp3);
if (skinMP3.isFile()) if (skinWAV.isFile())
return skinMP3.getAbsolutePath(); return skinWAV.getAbsolutePath();
if (skinMP3.isFile())
return skinMP3.getAbsolutePath();
}
if (ResourceLoader.resourceExists(wav)) if (ResourceLoader.resourceExists(wav))
return wav; return wav;
if (ResourceLoader.resourceExists(mp3)) 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); ErrorHandler.error(String.format("Could not find sound file '%s'.", s.getFileName()), null, false);
continue; 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++; currentFileIndex++;
} }
@@ -216,7 +226,14 @@ public class SoundController {
ErrorHandler.error(String.format("Could not find hit sound file '%s'.", filename), null, false); ErrorHandler.error(String.format("Could not find hit sound file '%s'.", filename), null, false);
continue; 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++; 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 hitSound the hit sound (bitmask)
* @param sampleSet the sample set * @param sampleSet the sample set
* @param additionSampleSet the 'addition' sample set * @param additionSampleSet the 'addition' sample set
@@ -276,16 +293,20 @@ public class SoundController {
return; return;
// play all sounds // play all sounds
HitSound.setSampleSet(sampleSet); if (hitSound == HitObject.SOUND_NORMAL || Options.getSkin().isLayeredHitSounds()) {
playClip(HitSound.NORMAL.getClip(), volume, null); HitSound.setSampleSet(sampleSet);
playClip(HitSound.NORMAL.getClip(), volume, null);
}
HitSound.setSampleSet(additionSampleSet); if (hitSound != HitObject.SOUND_NORMAL) {
if ((hitSound & OsuHitObject.SOUND_WHISTLE) > 0) HitSound.setSampleSet(additionSampleSet);
playClip(HitSound.WHISTLE.getClip(), volume, null); if ((hitSound & HitObject.SOUND_WHISTLE) > 0)
if ((hitSound & OsuHitObject.SOUND_FINISH) > 0) playClip(HitSound.WHISTLE.getClip(), volume, null);
playClip(HitSound.FINISH.getClip(), volume, null); if ((hitSound & HitObject.SOUND_FINISH) > 0)
if ((hitSound & OsuHitObject.SOUND_CLAP) > 0) playClip(HitSound.FINISH.getClip(), volume, null);
playClip(HitSound.CLAP.getClip(), volume, null); if ((hitSound & HitObject.SOUND_CLAP) > 0)
playClip(HitSound.CLAP.getClip(), volume, null);
}
} }
/** /**

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Beatmap> {
/** 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<Integer> breaks;
/**
* [TimingPoints]
*/
/** All timing points. */
public ArrayList<TimingPoint> 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<Integer>();
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<TimingPoint>();
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<Color> colors = new LinkedList<Color>();
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])));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<File, Image> cache;
/**
* Constructor.
*/
@SuppressWarnings("serial")
public BeatmapImageCache() {
this.cache = new LinkedHashMap<File, Image>(MAX_CACHE_SIZE + 1, 1.1f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<File, Image> 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.
* <p>
* 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(); }
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> stringdb = new HashMap<String, String>();
/** 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<String, Long> map = BeatmapDB.getLastModifiedMap();
// beatmap lists
List<ArrayList<Beatmap>> allBeatmaps = new LinkedList<ArrayList<Beatmap>>();
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // 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<Beatmap> beatmaps = new ArrayList<Beatmap>();
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<Beatmap> beatmaps : allBeatmaps) {
Collections.sort(beatmaps);
lastNode = BeatmapSetList.get().addSongGroup(beatmaps);
}
// clear string DB
stringdb = new HashMap<String, String>();
// 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<Beatmap> beatmaps, boolean parseObjects) {
Beatmap beatmap = new Beatmap(file);
beatmap.timingPoints = new ArrayList<TimingPoint>();
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<Integer>();
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<Color> colors = new LinkedList<Color>();
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Beatmap> beatmaps;
/**
* Constructor.
* @param beatmaps the beatmaps in this set
*/
public BeatmapSet(ArrayList<Beatmap> 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.
* <ul>
* <li>0: {Artist} - {Title} [{Version}]
* <li>1: Mapped by {Creator}
* <li>2: Length: {} BPM: {} Objects: {}
* <li>3: Circles: {} Sliders: {} Spinners: {}
* <li>4: CS:{} HP:{} AR:{} OD:{}
* </ul>
* @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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<BeatmapSetNode> 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<BeatmapSetNode> nodes;
/** Set of all beatmap set IDs for the parsed beatmaps. */
private HashSet<Integer> 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<BeatmapSetNode>();
MSIDdb = new HashSet<Integer>();
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<Beatmap> 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<String> terms = new LinkedList<String>(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<String> condType = new LinkedList<String>();
LinkedList<String> condOperator = new LinkedList<String>();
LinkedList<Float> condValue = new LinkedList<Float>();
Iterator<String> 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<BeatmapSetNode>();
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<BeatmapSetNode> 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<BeatmapSetNode> 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.
* <p>
* 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); }
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@@ -16,7 +16,11 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
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.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -25,9 +29,9 @@ import java.util.Comparator;
import org.newdawn.slick.Image; import org.newdawn.slick.Image;
/** /**
* OsuGroupNode sorts. * Beatmap sorting orders.
*/ */
public enum SongSort { public enum BeatmapSortOrder {
TITLE (0, "Title", new TitleOrder()), TITLE (0, "Title", new TitleOrder()),
ARTIST (1, "Artist", new ArtistOrder()), ARTIST (1, "Artist", new ArtistOrder()),
CREATOR (2, "Creator", new CreatorOrder()), CREATOR (2, "Creator", new CreatorOrder()),
@@ -41,7 +45,7 @@ public enum SongSort {
private String name; private String name;
/** The comparator for the sort. */ /** The comparator for the sort. */
private Comparator<OsuGroupNode> comparator; private Comparator<BeatmapSetNode> comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */ /** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab; private MenuButton tab;
@@ -49,83 +53,85 @@ public enum SongSort {
/** Total number of sorts. */ /** Total number of sorts. */
private static final int SIZE = values().length; private static final int SIZE = values().length;
/** Array of SongSort objects in reverse order. */ /** Array of BeatmapSortOrder objects in reverse order. */
public static final SongSort[] VALUES_REVERSED; public static final BeatmapSortOrder[] VALUES_REVERSED;
static { static {
VALUES_REVERSED = values(); VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED)); Collections.reverse(Arrays.asList(VALUES_REVERSED));
} }
/** Current sort. */ /** Current sort. */
private static SongSort currentSort = TITLE; private static BeatmapSortOrder currentSort = TITLE;
/** /**
* Returns the current sort. * Returns the current sort.
* @return the current sort * @return the current sort
*/ */
public static SongSort getSort() { return currentSort; } public static BeatmapSortOrder getSort() { return currentSort; }
/** /**
* Sets a new sort. * Sets a new sort.
* @param sort the 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<OsuGroupNode> { private static class TitleOrder implements Comparator<BeatmapSetNode> {
@Override @Override
public int compare(OsuGroupNode v, OsuGroupNode w) { public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return v.osuFiles.get(0).title.compareToIgnoreCase(w.osuFiles.get(0).title); 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<OsuGroupNode> { private static class ArtistOrder implements Comparator<BeatmapSetNode> {
@Override @Override
public int compare(OsuGroupNode v, OsuGroupNode w) { public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return v.osuFiles.get(0).artist.compareToIgnoreCase(w.osuFiles.get(0).artist); 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<OsuGroupNode> { private static class CreatorOrder implements Comparator<BeatmapSetNode> {
@Override @Override
public int compare(OsuGroupNode v, OsuGroupNode w) { public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return v.osuFiles.get(0).creator.compareToIgnoreCase(w.osuFiles.get(0).creator); 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<OsuGroupNode> { private static class BPMOrder implements Comparator<BeatmapSetNode> {
@Override @Override
public int compare(OsuGroupNode v, OsuGroupNode w) { public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return Integer.compare(v.osuFiles.get(0).bpmMax, w.osuFiles.get(0).bpmMax); 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. * Uses the longest beatmap in each set for comparison.
*/ */
private static class LengthOrder implements Comparator<OsuGroupNode> { private static class LengthOrder implements Comparator<BeatmapSetNode> {
@Override @Override
public int compare(OsuGroupNode v, OsuGroupNode w) { public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vMax = 0, wMax = 0; int vMax = 0, wMax = 0;
for (OsuFile osu : v.osuFiles) { for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
if (osu.endTime > vMax) Beatmap beatmap = v.getBeatmapSet().get(i);
vMax = osu.endTime; if (beatmap.endTime > vMax)
vMax = beatmap.endTime;
} }
for (OsuFile osu : w.osuFiles) { for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
if (osu.endTime > wMax) Beatmap beatmap = w.getBeatmapSet().get(i);
wMax = osu.endTime; if (beatmap.endTime > wMax)
wMax = beatmap.endTime;
} }
return Integer.compare(vMax, wMax); return Integer.compare(vMax, wMax);
} }
@@ -137,7 +143,7 @@ public enum SongSort {
* @param name the sort name * @param name the sort name
* @param comparator the comparator for the sort * @param comparator the comparator for the sort
*/ */
SongSort(int id, String name, Comparator<OsuGroupNode> comparator) { BeatmapSortOrder(int id, String name, Comparator<BeatmapSetNode> comparator) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.comparator = comparator; this.comparator = comparator;
@@ -167,7 +173,7 @@ public enum SongSort {
* Returns the comparator for the sort. * Returns the comparator for the sort.
* @return the comparator * @return the comparator
*/ */
public Comparator<OsuGroupNode> getComparator() { return comparator; } public Comparator<BeatmapSetNode> getComparator() { return comparator; }
/** /**
* Checks if the coordinates are within the image bounds. * Checks if the coordinates are within the image bounds.

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@@ -16,14 +16,16 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu; package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.Utils;
import org.newdawn.slick.util.Log; import org.newdawn.slick.util.Log;
/** /**
* Data type representing a timing point. * Data type representing a timing point.
*/ */
public class OsuTimingPoint { public class TimingPoint {
/** Timing point start time/offset (in ms). */ /** Timing point start time/offset (in ms). */
private int time = 0; private int time = 0;
@@ -55,7 +57,7 @@ public class OsuTimingPoint {
* Constructor. * Constructor.
* @param line the line to be parsed * @param line the line to be parsed
*/ */
public OsuTimingPoint(String line) { public TimingPoint(String line) {
// TODO: better support for old formats // TODO: better support for old formats
String[] tokens = line.split(","); String[] tokens = line.split(",");
try { try {
@@ -64,9 +66,9 @@ public class OsuTimingPoint {
this.sampleType = Byte.parseByte(tokens[3]); this.sampleType = Byte.parseByte(tokens[3]);
this.sampleTypeCustom = Byte.parseByte(tokens[4]); this.sampleTypeCustom = Byte.parseByte(tokens[4]);
this.sampleVolume = Integer.parseInt(tokens[5]); this.sampleVolume = Integer.parseInt(tokens[5]);
// this.inherited = (Integer.parseInt(tokens[6]) == 1); // this.inherited = Utils.parseBoolean(tokens[6]);
if (tokens.length > 7) if (tokens.length > 7)
this.kiai = (Integer.parseInt(tokens[7]) == 1); this.kiai = Utils.parseBoolean(tokens[7]);
} catch (ArrayIndexOutOfBoundsException e) { } catch (ArrayIndexOutOfBoundsException e) {
Log.debug(String.format("Error parsing timing point: '%s'", line)); Log.debug(String.format("Error parsing timing point: '%s'", line));
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Beatmap> 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<Beatmap> 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<String, HashMap<String, Beatmap>> map = new HashMap<String, HashMap<String, Beatmap>>();
for (Beatmap beatmap : batch) {
String parent = beatmap.getFile().getParentFile().getName();
String name = beatmap.getFile().getName();
HashMap<String, Beatmap> m = map.get(parent);
if (m == null) {
m = new HashMap<String, Beatmap>();
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<String, Beatmap> 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<String, Long> getLastModifiedMap() {
if (connection == null)
return null;
try (Statement stmt = connection.createStatement()) {
Map<String, Long> map = new HashMap<String, Long>();
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);
}
}
}

View File

@@ -43,7 +43,7 @@ public class DBController {
} }
// initialize the databases // initialize the databases
OsuDB.init(); BeatmapDB.init();
ScoreDB.init(); ScoreDB.init();
} }
@@ -51,7 +51,7 @@ public class DBController {
* Closes all database connections. * Closes all database connections.
*/ */
public static void closeConnections() { public static void closeConnections() {
OsuDB.closeConnection(); BeatmapDB.closeConnection();
ScoreDB.closeConnection(); ScoreDB.closeConnection();
} }

View File

@@ -1,3 +1,5 @@
//todo rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -20,8 +20,8 @@ package itdelatrisu.opsu.db;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.beatmap.Beatmap;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
@@ -249,18 +249,18 @@ public class ScoreDB {
/** /**
* Deletes all the scores for the given beatmap from the database. * 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) if (connection == null)
return; return;
try { try {
deleteSongStmt.setInt(1, osu.beatmapID); deleteSongStmt.setInt(1, beatmap.beatmapID);
deleteSongStmt.setString(2, osu.title); deleteSongStmt.setString(2, beatmap.title);
deleteSongStmt.setString(3, osu.artist); deleteSongStmt.setString(3, beatmap.artist);
deleteSongStmt.setString(4, osu.creator); deleteSongStmt.setString(4, beatmap.creator);
deleteSongStmt.setString(5, osu.version); deleteSongStmt.setString(5, beatmap.version);
deleteSongStmt.executeUpdate(); deleteSongStmt.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Failed to delete scores from database.", e, true); 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. * Retrieves the game scores for a beatmap.
* @param osu the OsuFile * @param beatmap the beatmap
* @return all scores for the beatmap, or null if any error occurred * @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) if (connection == null)
return null; return null;
List<ScoreData> list = new ArrayList<ScoreData>(); List<ScoreData> list = new ArrayList<ScoreData>();
try { try {
selectMapStmt.setInt(1, osu.beatmapID); selectMapStmt.setInt(1, beatmap.beatmapID);
selectMapStmt.setString(2, osu.title); selectMapStmt.setString(2, beatmap.title);
selectMapStmt.setString(3, osu.artist); selectMapStmt.setString(3, beatmap.artist);
selectMapStmt.setString(4, osu.creator); selectMapStmt.setString(4, beatmap.creator);
selectMapStmt.setString(5, osu.version); selectMapStmt.setString(5, beatmap.version);
ResultSet rs = selectMapStmt.executeQuery(); ResultSet rs = selectMapStmt.executeQuery();
while (rs.next()) { while (rs.next()) {
ScoreData s = new ScoreData(rs); ScoreData s = new ScoreData(rs);
@@ -327,21 +327,21 @@ public class ScoreDB {
} }
/** /**
* Retrieves the game scores for an OsuFile map set. * Retrieves the game scores for a beatmap set.
* @param osu the OsuFile * @param beatmap the beatmap
* @return all scores for the beatmap set (Version, ScoreData[]), * @return all scores for the beatmap set (Version, ScoreData[]),
* or null if any error occurred * or null if any error occurred
*/ */
public static Map<String, ScoreData[]> getMapSetScores(OsuFile osu) { public static Map<String, ScoreData[]> getMapSetScores(Beatmap beatmap) {
if (connection == null) if (connection == null)
return null; return null;
Map<String, ScoreData[]> map = new HashMap<String, ScoreData[]>(); Map<String, ScoreData[]> map = new HashMap<String, ScoreData[]>();
try { try {
selectMapSetStmt.setInt(1, osu.beatmapSetID); selectMapSetStmt.setInt(1, beatmap.beatmapSetID);
selectMapSetStmt.setString(2, osu.title); selectMapSetStmt.setString(2, beatmap.title);
selectMapSetStmt.setString(3, osu.artist); selectMapSetStmt.setString(3, beatmap.artist);
selectMapSetStmt.setString(4, osu.creator); selectMapSetStmt.setString(4, beatmap.creator);
ResultSet rs = selectMapSetStmt.executeQuery(); ResultSet rs = selectMapSetStmt.executeQuery();
List<ScoreData> list = null; List<ScoreData> list = null;

View File

@@ -21,11 +21,12 @@ package itdelatrisu.opsu.downloads;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.Download.DownloadListener; import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.downloads.Download.Status; import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
@@ -85,7 +86,7 @@ public class DownloadNode {
buttonBaseX = width * 0.024f; buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f; buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f; buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2f; buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
buttonOffset = buttonHeight * 1.1f; buttonOffset = buttonHeight * 1.1f;
// download info // download info
@@ -239,22 +240,26 @@ public class DownloadNode {
* @see #getDownload() * @see #getDownload()
*/ */
public void createDownload(DownloadServer server) { public void createDownload(DownloadServer server) {
if (download == null) { if (download != null)
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID); return;
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()));
}
@Override String url = server.getDownloadURL(beatmapSetID);
public void error() { if (url == null)
UI.sendBarNotification("Download failed due to a connection error."); 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); g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// map is already loaded // map is already loaded
if (OsuGroupList.get().containsBeatmapSetID(beatmapSetID)) { if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
g.setColor(Utils.COLOR_BLUE_BUTTON); g.setColor(Utils.COLOR_BLUE_BUTTON);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight); g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
} }
@@ -340,13 +345,15 @@ public class DownloadNode {
textX += img.getWidth() + buttonWidth * 0.001f; textX += img.getWidth() + buttonWidth * 0.001f;
// text // 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 if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist()); 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( Utils.FONT_BOLD.drawString(
textX, y + marginY, textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(), String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white); (dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
g.clearClip();
Utils.FONT_DEFAULT.drawString( Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white); String.format("Last updated: %s", date), Color.white);

View File

@@ -20,14 +20,15 @@ package itdelatrisu.opsu.downloads;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.Download.DownloadListener; import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@@ -35,6 +36,7 @@ import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader; import org.newdawn.slick.util.ResourceLoader;
/** /**
@@ -206,7 +208,12 @@ public class Updater {
return; return;
// get latest version // 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) { if (s == null) {
status = Status.CONNECTION_ERROR; status = Status.CONNECTION_ERROR;
return; return;

View File

@@ -16,25 +16,32 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu.downloads; package itdelatrisu.opsu.downloads.servers;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; 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.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
/** /**
* Download server: http://bloodcat.com/osu/ * Download server: http://bloodcat.com/osu/
*/ */
public class BloodcatServer extends DownloadServer { public class BloodcatServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "Bloodcat";
/** Formatted download URL: {@code beatmapSetID} */ /** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d"; private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d";
@@ -48,7 +55,10 @@ public class BloodcatServer extends DownloadServer {
public BloodcatServer() {} public BloodcatServer() {}
@Override @Override
public String getURL(int beatmapSetID) { public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID); return String.format(DOWNLOAD_URL, beatmapSetID);
} }
@@ -58,7 +68,7 @@ public class BloodcatServer extends DownloadServer {
try { try {
// read JSON // read JSON
String search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), rankedOnly ? "0" : "", page); 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) { if (json == null) {
this.totalResults = -1; this.totalResults = -1;
return null; return null;
@@ -70,7 +80,7 @@ public class BloodcatServer extends DownloadServer {
for (int i = 0; i < nodes.length; i++) { for (int i = 0; i < nodes.length; i++) {
JSONObject item = arr.getJSONObject(i); JSONObject item = arr.getJSONObject(i);
nodes[i] = new DownloadNode( 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("title"), item.isNull("titleUnicode") ? null : item.getString("titleUnicode"),
item.getString("artist"), item.isNull("artistUnicode") ? null : item.getString("artistUnicode"), item.getString("artist"), item.isNull("artistUnicode") ? null : item.getString("artistUnicode"),
item.getString("creator") item.getString("creator")
@@ -85,25 +95,31 @@ public class BloodcatServer extends DownloadServer {
return nodes; return nodes;
} }
@Override
public int minQueryLength() { return 0; }
@Override @Override
public int totalResults() { return totalResults; } public int totalResults() { return totalResults; }
/** /**
* Returns a JSON object from a URL. * Returns a formatted date string from a raw date.
* @param url the remote URL * @param s the raw date string (e.g. "2015-05-14T23:38:47+09:00")
* @return the JSON object * @return the formatted date, or the raw string if it could not be parsed
* @author Roland Illig (http://stackoverflow.com/a/4308662)
*/ */
private static JSONObject readJsonFromUrl(URL url) throws IOException { private String formatDate(String s) {
String s = Utils.readDataFromUrl(url); try {
JSONObject json = null; // make string parseable by SimpleDateFormat
if (s != null) { int index = s.lastIndexOf(':');
try { if (index == -1)
json = new JSONObject(s); return s;
} catch (JSONException e) { String str = new StringBuilder(s).deleteCharAt(index).toString();
ErrorHandler.error("Failed to create JSON object.", e, true);
} 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;
} }
} }

View File

@@ -16,7 +16,9 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu.downloads; package itdelatrisu.opsu.downloads.servers;
import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException; import java.io.IOException;
@@ -27,12 +29,18 @@ public abstract class DownloadServer {
/** Track preview URL. */ /** Track preview URL. */
private static final String PREVIEW_URL = "http://b.ppy.sh/preview/%d.mp3"; 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. * Returns a web address to download the given beatmap.
* @param beatmapSetID the beatmap set ID * @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 * 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; 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. * Returns the total number of results for the last search query.
* This will differ from the the size of the array returned by * This will differ from the the size of the array returned by

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer, Integer> idTable = new HashMap<Integer, Integer>();
/** 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;
}
}
}

View File

@@ -19,10 +19,12 @@
package itdelatrisu.opsu.objects; package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.Game;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
@@ -32,14 +34,14 @@ import org.newdawn.slick.Graphics;
/** /**
* Data type representing a circle object. * 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. */ /** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375; private static final int FADE_IN_TIME = 375;
private static float diameter; private static float diameter;
/** The associated OsuHitObject. */ /** The associated HitObject. */
private OsuHitObject hitObject; private HitObject hitObject;
/** The scaled starting x, y coordinates. */ /** The scaled starting x, y coordinates. */
private float x, y; private float x, y;
@@ -69,17 +71,22 @@ public class Circle implements HitObject {
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
/*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. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param game the associated Game object * @param game the associated Game object
* @param data the associated GameData object * @param data the associated GameData object
* @param color the color of this circle * @param color the color of this circle
* @param comboEnd true if this is the last hit object in the combo * @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.hitObject = hitObject;
this.game = game; this.game = game;
this.data = data; this.data = data;
@@ -102,9 +109,13 @@ public class Circle implements HitObject {
if (timeDiff >= 0) if (timeDiff >= 0)
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color); GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
GameImage.HITCIRCLE.getImage().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, data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); 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; Utils.COLOR_WHITE_FADE.a = oldAlpha;
} }
@@ -144,7 +155,7 @@ public class Circle implements HitObject {
if (result > -1) { if (result > -1) {
data.addHitError(hitObject.getTime(), x, y, timeDiff); 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; return true;
} }
} }
@@ -160,17 +171,17 @@ public class Circle implements HitObject {
if (trackPosition > time + hitResultOffset[GameData.HIT_50]) { if (trackPosition > time + hitResultOffset[GameData.HIT_50]) {
if (isAutoMod) // "auto" mod: catch any missed notes due to lag if (isAutoMod) // "auto" mod: catch any missed notes due to lag
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0); 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 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; return true;
} }
// "auto" mod: send a perfect hit result // "auto" mod: send a perfect hit result
else if (isAutoMod) { else if (isAutoMod) {
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) { if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0); data.hitResult(time, GameData.HIT_300, x, y, color, comboEnd, hitObject, 0, HitObjectType.CIRCLE, null, true);
return true; return true;
} }
} }

View File

@@ -18,25 +18,25 @@
package itdelatrisu.opsu.objects; package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.beatmap.HitObject;
import org.newdawn.slick.Graphics; 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 { public class DummyObject implements GameObject {
/** The associated OsuHitObject. */ /** The associated HitObject. */
private OsuHitObject hitObject; private HitObject hitObject;
/** The scaled starting x, y coordinates. */ /** The scaled starting x, y coordinates. */
private float x, y; private float x, y;
/** /**
* Constructor. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
*/ */
public DummyObject(OsuHitObject hitObject) { public DummyObject(HitObject hitObject) {
this.hitObject = hitObject; this.hitObject = hitObject;
updatePosition(); updatePosition();
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}

View File

@@ -1,3 +1,5 @@
//TODO rename
/* /*
* opsu! - an open-source osu! client * opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han * Copyright (C) 2014, 2015 Jeffrey Han

View File

@@ -19,11 +19,14 @@
package itdelatrisu.opsu.objects; package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils; 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.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier; import itdelatrisu.opsu.objects.curves.LinearBezier;
@@ -37,7 +40,7 @@ import org.newdawn.slick.Image;
/** /**
* Data type representing a slider object. * Data type representing a slider object.
*/ */
public class Slider implements HitObject { public class Slider implements GameObject {
/** Slider ball frames. */ /** Slider ball frames. */
private static Image[] sliderBallImages; private static Image[] sliderBallImages;
@@ -54,8 +57,8 @@ public class Slider implements HitObject {
/** The amount of time, in milliseconds, to fade in the slider. */ /** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375; private static final int FADE_IN_TIME = 375;
/** The associated OsuHitObject. */ /** The associated HitObject. */
private OsuHitObject hitObject; private HitObject hitObject;
/** The scaled starting x, y coordinates. */ /** The scaled starting x, y coordinates. */
protected float x, y; protected float x, y;
@@ -108,9 +111,9 @@ public class Slider implements HitObject {
* Initializes the Slider data type with images and dimensions. * Initializes the Slider data type with images and dimensions.
* @param container the game container * @param container the game container
* @param circleSize the map's circleSize value * @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(); containerWidth = container.getWidth();
containerHeight = container.getHeight(); containerHeight = container.getHeight();
@@ -119,7 +122,11 @@ public class Slider implements HitObject {
int diameterInt = (int)diameter; int diameterInt = (int)diameter;
followRadius = diameter / 2 * 3f; followRadius = diameter / 2 * 3f;
//TODO conflict
/*
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
*/
// slider ball // slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() || if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
@@ -133,19 +140,19 @@ public class Slider implements HitObject {
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt)); GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4)); GameImage.SLIDER_TICK.setImage(GameImage.SLIDER_TICK.getImage().getScaledCopy(diameterInt / 4, diameterInt / 4));
sliderMultiplier = osu.sliderMultiplier; sliderMultiplier = beatmap.sliderMultiplier;
sliderTickRate = osu.sliderTickRate; sliderTickRate = beatmap.sliderTickRate;
} }
/** /**
* Constructor. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param game the associated Game object * @param game the associated Game object
* @param data the associated GameData object * @param data the associated GameData object
* @param color the color of this slider * @param color the color of this slider
* @param comboEnd true if this is the last hit object in the combo * @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.hitObject = hitObject;
this.game = game; this.game = game;
this.data = data; 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 fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
float approachScale = 1 + scale * 3; float approachScale = 1 + scale * 3;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1); float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
float oldAlpha = Utils.COLOR_WHITE_FADE.a; float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha; 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(color);
curve.draw(); 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 // ticks
if (ticksT != null) { if (ticksT != null) {
@@ -191,23 +211,13 @@ public class Slider implements HitObject {
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE); 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) if (sliderClickedInitial)
; // don't draw current combo number if already clicked ; // don't draw current combo number if already clicked
else else
data.drawSymbolNumber(hitObject.getComboNumber(), x, y, 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 // repeats
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { 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[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
float t = getT(trackPosition, false); 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) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % 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); 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 else
result = GameData.HIT_MISS; result = GameData.HIT_MISS;
float cx, cy;
HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle if (currentRepeats % 2 == 0) { // last circle
float[] lastPos = curve.pointAt(1); float[] lastPos = curve.pointAt(1);
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, cx = lastPos[0];
lastPos[0], lastPos[1], color, comboEnd, hitObject, currentRepeats + 1); cy = lastPos[1];
type = HitObjectType.SLIDER_LAST;
} else { // first circle } else { // first circle
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, cx = x;
x, y, color, comboEnd, hitObject, currentRepeats + 1); 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; return result;
} }
@@ -509,10 +525,12 @@ public class Slider implements HitObject {
this.x = hitObject.getScaledX(); this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY(); 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); this.curve = new CircumscribedCircle(hitObject, color);
else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
this.curve = new CatmullCurve(hitObject, color);
else else
this.curve = new LinearBezier(hitObject, color); this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
} }
@Override @Override

View File

@@ -19,12 +19,14 @@
package itdelatrisu.opsu.objects; package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameData.HitObjectType;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.Game;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
@@ -35,7 +37,7 @@ import org.newdawn.slick.Image;
/** /**
* Data type representing a spinner object. * Data type representing a spinner object.
*/ */
public class Spinner implements HitObject { public class Spinner implements GameObject {
/** Container dimensions. */ /** Container dimensions. */
private static int width, height; 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 private static final float MAX_ANG_DIFF = DELTA_UPDATE_TIME * 477 / 60 / 1000 * TWO_PI; // ~95.3
/** The associated OsuHitObject. */ /** The associated HitObject. */
private OsuHitObject hitObject; private HitObject hitObject;
/** The associated GameData object. */ /** The associated GameData object. */
private GameData data; private GameData data;
@@ -110,11 +112,11 @@ public class Spinner implements HitObject {
/** /**
* Constructor. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param game the associated Game object * @param game the associated Game object
* @param data the associated GameData 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.hitObject = hitObject;
this.data = data; this.data = data;
@@ -132,15 +134,17 @@ public class Spinner implements HitObject {
return; return;
boolean spinnerComplete = (rotations >= rotationsNeeded); boolean spinnerComplete = (rotations >= rotationsNeeded);
float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
// darken screen // darken screen
float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f); if (Options.getSkin().isSpinnerFadePlayfield()) {
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a; float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
if (timeDiff > 0) if (timeDiff > 0)
Utils.COLOR_BLACK_ALPHA.a *= alpha; Utils.COLOR_BLACK_ALPHA.a *= alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA); g.setColor(Utils.COLOR_BLACK_ALPHA);
g.fillRect(0, 0, width, height); g.fillRect(0, 0, width, height);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha; Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
}
// rpm // rpm
Image rpmImg = GameImage.SPINNER_RPM.getImage(); Image rpmImg = GameImage.SPINNER_RPM.getImage();
@@ -198,7 +202,7 @@ public class Spinner implements HitObject {
result = GameData.HIT_MISS; result = GameData.HIT_MISS;
data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2, data.hitResult(hitObject.getEndTime(), result, width / 2, height / 2,
Color.transparent, true, hitObject, 0); Color.transparent, true, hitObject, 0, HitObjectType.SPINNER, null, true);
return result; return result;
} }

View File

@@ -23,22 +23,10 @@ package itdelatrisu.opsu.objects.curves;
* *
* @author fluddokt (https://github.com/fluddokt) * @author fluddokt (https://github.com/fluddokt)
*/ */
public class Bezier2 { public class Bezier2 extends CurveType {
/** The control points of the Bezier curve. */ /** The control points of the Bezier curve. */
private Vec2f[] points; 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. * Constructor.
* @param points the control points * @param points the control points
@@ -52,27 +40,10 @@ public class Bezier2 {
for (int i = 0; i < points.length - 1; i++) for (int i = 0; i < points.length - 1; i++)
approxlength += points[i].cpy().sub(points[i + 1]).len(); approxlength += points[i].cpy().sub(points[i + 1]).len();
// subdivide the curve init(approxlength);
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());
} }
/** @Override
* Returns the point on the Bezier curve at a value t.
* @param t the t value [0, 1]
* @return the point [x, y]
*/
public Vec2f pointAt(float t) { public Vec2f pointAt(float t) {
Vec2f c = new Vec2f(); Vec2f c = new Vec2f();
int n = points.length - 1; int n = points.length - 1;
@@ -84,31 +55,11 @@ public class Bezier2 {
return c; 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. * Calculates the binomial coefficient.
* http://en.wikipedia.org/wiki/Binomial_coefficient#Binomial_coefficient_in_programming_languages * 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) if (k < 0 || k > n)
return 0; return 0;
if (k == 0 || k == n) if (k == 0 || k == n)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<CurveType> catmulls = new LinkedList<CurveType>();
int ncontrolPoints = hitObject.getSliderX().length + 1;
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // 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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
import org.newdawn.slick.SlickException;
/**
* Representation of a Centripetal CatmullRom spline.
* (Currently not technically Centripetal CatmullRom.)
* 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;
}
}

View File

@@ -19,12 +19,9 @@
package itdelatrisu.opsu.objects.curves; package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
/** /**
* Representation of a curve along a Circumscribed Circle of three points. * 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. */ /** The start and end angles for drawing. */
private float drawStartAngle, drawEndAngle; private float drawStartAngle, drawEndAngle;
/** The number of steps in the curve to draw. */
private float step;
/** Points along the curve. */
private Vec2f[] curve;
/** /**
* Constructor. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param color the color of this curve * @param color the color of this curve
*/ */
public CircumscribedCircle(OsuHitObject hitObject, Color color) { public CircumscribedCircle(HitObject hitObject, Color color) {
super(hitObject, color); super(hitObject, color);
this.step = hitObject.getPixelLength() / 5f;
// construct the three points // construct the three points
this.start = new Vec2f(getX(0), getY(0)); this.start = new Vec2f(getX(0), getY(0));
this.mid = new Vec2f(getX(1), getY(1)); 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 // find an angle with an arc length of pixelLength along this circle
this.radius = startAngPoint.len(); 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 float arcAng = pixelLength / radius; // len = theta * r / theta = len / r
// now use it for our new end angle // 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); this.drawStartAngle = (float) ((startAng + (startAng > endAng ? -HALF_PI : HALF_PI)) * 180 / Math.PI);
// calculate points // calculate points
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
curve = new Vec2f[(int) step + 1]; curve = new Vec2f[(int) step + 1];
for (int i = 0; i < curve.length; i++) { for (int i = 0; i < curve.length; i++) {
float[] xy = pointAt(i / step); 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 @Override
public float getEndAngle() { return drawEndAngle; } public float getEndAngle() { return drawEndAngle; }

View File

@@ -18,9 +18,17 @@
package itdelatrisu.opsu.objects.curves; 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.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
/** /**
* Representation of a curve. * Representation of a curve.
@@ -28,11 +36,17 @@ import org.newdawn.slick.Color;
* @author fluddokt (https://github.com/fluddokt) * @author fluddokt (https://github.com/fluddokt)
*/ */
public abstract class Curve { public abstract class Curve {
/** The associated OsuHitObject. */ /** Points generated along the curve should be spaced this far apart. */
protected OsuHitObject hitObject; protected static float CURVE_POINTS_SEPERATION = 5;
/** The color of this curve. */ /** The curve border color. */
protected Color 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. */ /** The scaled starting x, y coordinates. */
protected float x, y; protected float x, y;
@@ -40,18 +54,43 @@ public abstract class Curve {
/** The scaled slider x, y coordinate lists. */ /** The scaled slider x, y coordinate lists. */
protected float[] sliderX, sliderY; 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. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param color the color of this curve * @param color the color of this curve
*/ */
protected Curve(OsuHitObject hitObject, Color color) { protected Curve(HitObject hitObject, Color color) {
this.hitObject = hitObject; this.hitObject = hitObject;
this.x = hitObject.getScaledX(); this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY(); this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX(); this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY(); 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. * 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. * 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) { protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t; return a * (1 - t) + b * t;
} }
/**
* Discards the slider cache (only used for mmsliders).
*/
public void discardCache() {
if (renderState != null)
renderState.discardCache();
}
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<CurveType> 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<CurveType> 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; }
}

View File

@@ -18,50 +18,46 @@
package itdelatrisu.opsu.objects.curves; package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import org.newdawn.slick.Color; 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 * http://pomax.github.io/bezierinfo/#tracing
* *
* @author fluddokt (https://github.com/fluddokt) * @author fluddokt (https://github.com/fluddokt)
*/ */
public class LinearBezier extends Curve { public class LinearBezier extends EqualDistanceMultiCurve {
/** 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<Bezier2> beziers = new LinkedList<Bezier2>();
/** Points along the curve at equal distance. */
private Vec2f[] curve;
/** The number of points along the curve. */
private int ncurve;
/** /**
* Constructor. * Constructor.
* @param hitObject the associated OsuHitObject * @param hitObject the associated HitObject
* @param color the color of this curve * @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); super(hitObject, color);
// splits points into different Beziers if has the same points (red points) LinkedList<CurveType> beziers = new LinkedList<CurveType>();
// 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; int controlPoints = hitObject.getSliderX().length + 1;
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different Bezier curves LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different Bezier curves
Vec2f lastPoi = null; Vec2f lastPoi = null;
for (int i = 0; i < controlPoints; i++) { for (int i = 0; i < controlPoints; i++) {
Vec2f tpoi = new Vec2f(getX(i), getY(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) if (points.size() >= 2)
beziers.add(new Bezier2(points.toArray(new Vec2f[0]))); beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
points.clear(); points.clear();
@@ -69,7 +65,7 @@ public class LinearBezier extends Curve {
points.add(tpoi); points.add(tpoi);
lastPoi = tpoi; lastPoi = tpoi;
} }
if (points.size() < 2) { if (line || points.size() < 2) {
// trying to continue Bezier with less than 2 points // trying to continue Bezier with less than 2 points
// probably ending on a red point, just ignore it // probably ending on a red point, just ignore it
} else { } else {
@@ -77,105 +73,6 @@ public class LinearBezier extends Curve {
points.clear(); points.clear();
} }
// find the length of all beziers init(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<Bezier2> 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);
// }
} }
@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; }
} }

View File

@@ -49,6 +49,28 @@ public class Vec2f {
return new Vec2f((x + o.x) / 2, (y + o.y) / 2); 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. * Subtracts a vector from this vector.
* @param o the other vector * @param o the other vector
@@ -97,4 +119,7 @@ public class Vec2f {
* @return true if the two vectors are numerically equal * @return true if the two vectors are numerically equal
*/ */
public boolean equals(Vec2f o) { return (x == o.x && y == o.y); } public boolean equals(Vec2f o) { return (x == o.x && y == o.y); }
@Override
public String toString() { return String.format("(%.3f, %.3f)", x, y); }
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 <dravorek (at) gmail.com>}
*/
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 <dravorek (at) gmail.com>}
*/
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");
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 <dravorek (at) gmail.com>}
*/
public class FrameBufferCache {
/** The single framebuffer cache instance. */
private static FrameBufferCache instance = null;
/** The mapping from hit objects to framebuffers. */
private Map<HitObject, Rendertarget> cacheMap;
/** */
private ArrayList<Rendertarget> 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<Rendertarget>();
cacheMap = new HashMap<HitObject, Rendertarget>();
}
/**
* 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 <dravorek (at) gmail.com>}
*/
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;
}
}

View File

@@ -16,13 +16,13 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu.replay;
/** /**
* Captures a single life frame. * Captures a single life frame.
* *
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/) * @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
*/ */
package itdelatrisu.opsu.replay;
public class LifeFrame { public class LifeFrame {
/** Time. */ /** Time. */
private int time; private int time;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -18,7 +18,7 @@
package itdelatrisu.opsu.replay; package itdelatrisu.opsu.replay;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.beatmap.HitObject;
/** /**
* Captures a single replay frame. * Captures a single replay frame.
@@ -98,12 +98,12 @@ public class ReplayFrame {
/** /**
* Returns the scaled cursor x coordinate. * 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. * 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). * Returns the keys pressed (KEY_* bitmask).

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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.
* <ul>
* <li>1: peppysliders (segmented)
* <li>2: mmsliders (smooth)
* <li>3: toonsliders (smooth, with steps instead of gradient)
* <li>4: legacy OpenGL-only sliders
* </ul>
* @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; }
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<File> dirs = new ArrayList<File>();
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<Color> colors = new LinkedList<Color>();
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;
}
}

View File

@@ -20,17 +20,17 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; 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.ArrayList;
import java.util.List; import java.util.List;
@@ -66,9 +66,9 @@ public class ButtonMenu extends BasicGameState {
BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) { BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) {
@Override @Override
public String[] getTitle(GameContainer container, StateBasedGame game) { public String[] getTitle(GameContainer container, StateBasedGame game) {
OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
String osuString = (node != null) ? OsuGroupList.get().getBaseNode(node.index).toString() : ""; String beatmapString = (node != null) ? BeatmapSetList.get().getBaseNode(node.index).toString() : "";
return new String[] { osuString, "What do you want to do with this beatmap?" }; return new String[] { beatmapString, "What do you want to do with this beatmap?" };
} }
@Override @Override
@@ -86,9 +86,9 @@ public class ButtonMenu extends BasicGameState {
BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) { BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) {
@Override @Override
public String[] getTitle(GameContainer container, StateBasedGame game) { public String[] getTitle(GameContainer container, StateBasedGame game) {
OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
String osuString = (node != null) ? node.toString() : ""; String beatmapString = (node != null) ? node.toString() : "";
return new String[] { String.format("Are you sure you wish to delete '%s' from disk?", osuString) }; return new String[] { String.format("Are you sure you wish to delete '%s' from disk?", beatmapString) };
} }
@Override @Override
@@ -314,10 +314,10 @@ public class ButtonMenu extends BasicGameState {
public void draw(GameContainer container, StateBasedGame game, Graphics g) { public void draw(GameContainer container, StateBasedGame game, Graphics g) {
// draw title // draw title
if (actualTitle != null) { 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(); int lineHeight = Utils.FONT_LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++) 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 // draw buttons
@@ -451,7 +451,7 @@ public class ButtonMenu extends BasicGameState {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT); 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); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
} }
@@ -460,8 +460,8 @@ public class ButtonMenu extends BasicGameState {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
OsuGroupNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
MenuState ms = (node.osuFileIndex == -1 || node.osuFiles.size() == 1) ? MenuState ms = (node.beatmapIndex == -1 || node.getBeatmapSet().size() == 1) ?
MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT; MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, node); ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, node);
game.enterState(Opsu.STATE_BUTTONMENU); game.enterState(Opsu.STATE_BUTTONMENU);
@@ -478,7 +478,7 @@ public class ButtonMenu extends BasicGameState {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT); 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); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_CONFIRM, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
} }
@@ -493,7 +493,7 @@ public class ButtonMenu extends BasicGameState {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT); 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); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_DELETE_SELECT, node);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition(Color.black));
} }
@@ -582,7 +582,7 @@ public class ButtonMenu extends BasicGameState {
private MenuState menuState; private MenuState menuState;
/** The song node to process in the state. */ /** The song node to process in the state. */
private OsuGroupNode node; private BeatmapSetNode node;
/** The score data to process in the state. */ /** The score data to process in the state. */
private ScoreData scoreData; private ScoreData scoreData;
@@ -691,7 +691,7 @@ public class ButtonMenu extends BasicGameState {
* @param menuState the new menu state * @param menuState the new menu state
* @param node the song node to process in the 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. * Changes the menu state.
@@ -706,7 +706,7 @@ public class ButtonMenu extends BasicGameState {
* @param node the song node to process in the state * @param node the song node to process in the state
* @param scoreData the score scoreData * @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.menuState = menuState;
this.node = node; this.node = node;
this.scoreData = scoreData; this.scoreData = scoreData;
@@ -715,7 +715,7 @@ public class ButtonMenu extends BasicGameState {
/** /**
* Returns the song node being processed, or null if none. * 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. * Returns the score data being processed, or null if none.

View File

@@ -19,23 +19,25 @@
package itdelatrisu.opsu.states; package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuGroupNode;
import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; 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.Download;
import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode; 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.File;
import java.io.IOException; import java.io.IOException;
@@ -71,8 +73,11 @@ public class DownloadsMenu extends BasicGameState {
/** Minimum time, in milliseconds, that must elapse between queries. */ /** Minimum time, in milliseconds, that must elapse between queries. */
private static final int MIN_REQUEST_INTERVAL = 300; private static final int MIN_REQUEST_INTERVAL = 300;
/** The beatmap download server. */ /** Available beatmap download servers. */
private DownloadServer server = new BloodcatServer(); 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. */ /** The current list of search results. */
private DownloadNode[] resultList; private DownloadNode[] resultList;
@@ -138,7 +143,7 @@ public class DownloadsMenu extends BasicGameState {
private MenuButton prevPage, nextPage; private MenuButton prevPage, nextPage;
/** Buttons. */ /** Buttons. */
private MenuButton clearButton, importButton, resetButton, rankedButton; private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
/** Beatmap importing thread. */ /** Beatmap importing thread. */
private Thread importThread; private Thread importThread;
@@ -169,12 +174,12 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
float baseX = width * 0.024f; float baseX = width * 0.024f;
float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight(); float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
float searchWidth = width * 0.35f; float searchWidth = width * 0.3f;
// search // search
searchTimer = SEARCH_DELAY; searchTimer = SEARCH_DELAY;
searchResultString = "Type to search!"; searchResultString = "Loading data from server...";
search = new TextField( search = new TextField(
container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY, container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Utils.FONT_MEDIUM.getLineHeight() (int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
@@ -200,8 +205,10 @@ public class DownloadsMenu extends BasicGameState {
// buttons // buttons
float buttonMarginX = width * 0.004f; float buttonMarginX = width * 0.004f;
float buttonHeight = height * 0.038f; float buttonHeight = height * 0.038f;
float topButtonWidth = width * 0.14f; float resetWidth = width * 0.085f;
float lowerButtonWidth = width * 0.12f; float rankedWidth = width * 0.15f;
float serverWidth = width * 0.12f;
float lowerWidth = width * 0.12f;
float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f; float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f; float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
Image button = GameImage.MENU_BUTTON_MID.getImage(); Image button = GameImage.MENU_BUTTON_MID.getImage();
@@ -209,25 +216,33 @@ public class DownloadsMenu extends BasicGameState {
Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage(); Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage();
buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight()); buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight());
buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight()); buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight());
Image topButton = button.getScaledCopy((int) topButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight); int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
Image lowerButton = button.getScaledCopy((int) lowerButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight); Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
float fullTopButtonWidth = topButton.getWidth() + buttonL.getWidth() + buttonR.getWidth(); Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
float fullLowerButtonWidth = lowerButton.getWidth() + buttonL.getWidth() + buttonR.getWidth(); Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
clearButton = new MenuButton(lowerButton, buttonL, buttonR, Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
width * 0.75f + buttonMarginX + fullLowerButtonWidth / 2f, lowerButtonY); float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
importButton = new MenuButton(lowerButton, buttonL, buttonR, float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
width - buttonMarginX - fullLowerButtonWidth / 2f, lowerButtonY); float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
resetButton = new MenuButton(topButton, buttonL, buttonR, float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
baseX + searchWidth + buttonMarginX + fullTopButtonWidth / 2f, topButtonY); clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
rankedButton = new MenuButton(topButton, buttonL, buttonR, width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
baseX + searchWidth + buttonMarginX * 2f + fullTopButtonWidth * 3 / 2f, topButtonY); 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); clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
importButton.setText("Import All", 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(); clearButton.setHoverFade();
importButton.setHoverFade(); importButton.setHoverFade();
resetButton.setHoverFade(); resetButton.setHoverFade();
rankedButton.setHoverFade(); rankedButton.setHoverFade();
serverButton.setHoverFade();
} }
@Override @Override
@@ -241,7 +256,7 @@ public class DownloadsMenu extends BasicGameState {
GameImage.SEARCH_BG.getImage().draw(); GameImage.SEARCH_BG.getImage().draw();
// title // 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 // search
g.setColor(Color.white); g.setColor(Color.white);
@@ -317,6 +332,8 @@ public class DownloadsMenu extends BasicGameState {
resetButton.draw(Color.red); resetButton.draw(Color.red);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white); rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
rankedButton.draw(Color.magenta); rankedButton.draw(Color.magenta);
serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
serverButton.draw(Color.blue);
// importing beatmaps // importing beatmaps
if (importThread != null) { if (importThread != null) {
@@ -348,6 +365,7 @@ public class DownloadsMenu extends BasicGameState {
importButton.hoverUpdate(delta, mouseX, mouseY); importButton.hoverUpdate(delta, mouseX, mouseY);
resetButton.hoverUpdate(delta, mouseX, mouseY); resetButton.hoverUpdate(delta, mouseX, mouseY);
rankedButton.hoverUpdate(delta, mouseX, mouseY); rankedButton.hoverUpdate(delta, mouseX, mouseY);
serverButton.hoverUpdate(delta, mouseX, mouseY);
// focus timer // focus timer
if (focusResult != -1 && focusTimer < FOCUS_DELAY) if (focusResult != -1 && focusTimer < FOCUS_DELAY)
@@ -361,7 +379,9 @@ public class DownloadsMenu extends BasicGameState {
searchTimerReset = false; searchTimerReset = false;
final String query = search.getText().trim().toLowerCase(); 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; lastQuery = query;
lastQueryDir = pageDir; lastQueryDir = pageDir;
@@ -409,7 +429,7 @@ public class DownloadsMenu extends BasicGameState {
else { else {
if (query.isEmpty()) if (query.isEmpty())
searchResultString = "Type to search!"; searchResultString = "Type to search!";
else if (totalResults == 0) else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found."; searchResultString = "No results found.";
else else
searchResultString = String.format("%d result%s found!", searchResultString = String.format("%d result%s found!",
@@ -427,6 +447,14 @@ public class DownloadsMenu extends BasicGameState {
queryThread.start(); 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 @Override
@@ -463,7 +491,7 @@ public class DownloadsMenu extends BasicGameState {
final DownloadNode node = nodes[index]; final DownloadNode node = nodes[index];
// check if map is already loaded // check if map is already loaded
boolean isLoaded = OsuGroupList.get().containsBeatmapSetID(node.getID()); boolean isLoaded = BeatmapSetList.get().containsBeatmapSetID(node.getID());
// track preview // track preview
if (DownloadNode.resultIconContains(x, y, i)) { if (DownloadNode.resultIconContains(x, y, i)) {
@@ -481,7 +509,7 @@ public class DownloadsMenu extends BasicGameState {
} else { } else {
// play preview // play preview
try { try {
final URL url = new URL(server.getPreviewURL(node.getID())); final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
MusicController.pause(); MusicController.pause();
new Thread() { new Thread() {
@Override @Override
@@ -525,9 +553,13 @@ public class DownloadsMenu extends BasicGameState {
} else { } else {
// start download // start download
if (!DownloadList.get().contains(node.getID())) { if (!DownloadList.get().contains(node.getID())) {
DownloadList.get().addNode(node); node.createDownload(SERVERS[serverIndex]);
node.createDownload(server); if (node.getDownload() == null)
node.getDownload().start(); UI.sendBarNotification("The download could not be started.");
else {
DownloadList.get().addNode(node);
node.getDownload().start();
}
} }
} }
} else { } else {
@@ -584,15 +616,15 @@ public class DownloadsMenu extends BasicGameState {
// invoke unpacker and parser // invoke unpacker and parser
File[] dirs = OszUnpacker.unpackAllFiles(Options.getOSZDir(), Options.getBeatmapDir()); File[] dirs = OszUnpacker.unpackAllFiles(Options.getOSZDir(), Options.getBeatmapDir());
if (dirs != null && dirs.length > 0) { if (dirs != null && dirs.length > 0) {
OsuGroupNode node = OsuParser.parseDirectories(dirs); BeatmapSetNode node = BeatmapParser.parseDirectories(dirs);
if (node != null) { if (node != null) {
// stop preview // stop preview
previewID = -1; previewID = -1;
SoundController.stopTrack(); SoundController.stopTrack();
// initialize song list // initialize song list
OsuGroupList.get().reset(); BeatmapSetList.get().reset();
OsuGroupList.get().init(); BeatmapSetList.get().init();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(node, -1, true, true); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(node, -1, true, true);
// send notification // send notification
@@ -624,6 +656,22 @@ public class DownloadsMenu extends BasicGameState {
resetSearchTimer(); resetSearchTimer();
return; 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 // downloads
if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) { if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) {
@@ -700,7 +748,7 @@ public class DownloadsMenu extends BasicGameState {
switch (key) { switch (key) {
case Input.KEY_ESCAPE: case Input.KEY_ESCAPE:
if (importThread != null) { 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(); importThread.interrupt();
} else if (!search.getText().isEmpty()) { } else if (!search.getText().isEmpty()) {
// clear search text // clear search text
@@ -755,6 +803,7 @@ public class DownloadsMenu extends BasicGameState {
importButton.resetHover(); importButton.resetHover();
resetButton.resetHover(); resetButton.resetHover();
rankedButton.resetHover(); rankedButton.resetHover();
serverButton.resetHover();
focusResult = -1; focusResult = -1;
startResult = 0; startResult = 0;
startDownloadIndex = 0; startDownloadIndex = 0;

View File

@@ -22,29 +22,32 @@ import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; 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.ScoreData;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.HitSound;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.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.db.ScoreDB;
import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.Circle;
import itdelatrisu.opsu.objects.DummyObject; import itdelatrisu.opsu.objects.DummyObject;
import itdelatrisu.opsu.objects.HitObject; import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider; import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner; 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.Replay;
import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
import java.util.LinkedList; import java.util.LinkedList;
@@ -95,17 +98,17 @@ public class Game extends BasicGameState {
/** Stack position offset modifier. */ /** Stack position offset modifier. */
private static final float STACK_OFFSET_MODIFIER = 0.05f; private static final float STACK_OFFSET_MODIFIER = 0.05f;
/** The associated OsuFile object. */ /** The associated beatmap. */
private OsuFile osu; private Beatmap beatmap;
/** The associated GameData object. */ /** The associated GameData object. */
private GameData data; private GameData data;
/** Current hit object index in OsuHitObject[] array. */ /** Current hit object index (in both hit object arrays). */
private int objectIndex = 0; private int objectIndex = 0;
/** The map's HitObjects, indexed by objectIndex. */ /** The map's game objects, indexed by objectIndex. */
private HitObject[] hitObjects; private GameObject[] gameObjects;
/** Delay time, in milliseconds, before song starts. */ /** Delay time, in milliseconds, before song starts. */
private int leadInTime; private int leadInTime;
@@ -211,6 +214,9 @@ public class Game extends BasicGameState {
/** Whether or not the cursor should be pressed using the "auto" mod. */ /** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed; private boolean autoMousePressed;
/** Playback speed (used in replays and "auto" mod). */
private PlaybackSpeed playbackSpeed;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@@ -254,7 +260,7 @@ public class Game extends BasicGameState {
trackPosition = pauseTime; trackPosition = pauseTime;
else if (deathTime > -1) // "Easy" mod: health bar increasing else if (deathTime > -1) // "Easy" mod: health bar increasing
trackPosition = deathTime; trackPosition = deathTime;
int firstObjectTime = osu.objects[0].getTime(); int firstObjectTime = beatmap.objects[0].getTime();
int timeDiff = firstObjectTime - trackPosition; int timeDiff = firstObjectTime - trackPosition;
g.setBackground(Color.black); g.setBackground(Color.black);
@@ -269,11 +275,11 @@ public class Game extends BasicGameState {
float dimLevel = Options.getBackgroundDim(); float dimLevel = Options.getBackgroundDim();
if (trackPosition < firstObjectTime) { if (trackPosition < firstObjectTime) {
if (timeDiff < approachTime) if (timeDiff < approachTime)
dimLevel += (1f - dimLevel) * ((float) timeDiff / Math.min(approachTime, firstObjectTime)); dimLevel += (1f - dimLevel) * ((float) timeDiff / approachTime);
else else
dimLevel = 1f; 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(); Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel); playfield.setAlpha(dimLevel);
playfield.draw(); playfield.draw();
@@ -292,51 +298,51 @@ public class Game extends BasicGameState {
float[] autoXY = null; float[] autoXY = null;
if (isLeadIn()) { if (isLeadIn()) {
// lead-in // 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)); autoMouseY = (int) (height / (2f - progress));
} else if (objectIndex == 0 && trackPosition < firstObjectTime) { } else if (objectIndex == 0 && trackPosition < firstObjectTime) {
// before first object // before first object
timeDiff = firstObjectTime - trackPosition; timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) { if (timeDiff < approachTime) {
float[] xy = hitObjects[0].getPointAt(trackPosition); float[] xy = gameObjects[0].getPointAt(trackPosition);
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / Math.min(approachTime, firstObjectTime))); 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 // normal object
int objectTime = osu.objects[objectIndex].getTime(); int objectTime = beatmap.objects[objectIndex].getTime();
if (trackPosition < objectTime) { if (trackPosition < objectTime) {
float[] xyStart = hitObjects[objectIndex - 1].getPointAt(trackPosition); float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
int startTime = hitObjects[objectIndex - 1].getEndTime(); int startTime = gameObjects[objectIndex - 1].getEndTime();
if (osu.breaks != null && breakIndex < osu.breaks.size()) { if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
// starting a break: keep cursor at previous hit object position // 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; autoXY = xyStart;
// after a break ends: move startTime to break end time // after a break ends: move startTime to break end time
else if (breakIndex > 1) { else if (breakIndex > 1) {
int lastBreakEndTime = osu.breaks.get(breakIndex - 1); int lastBreakEndTime = beatmap.breaks.get(breakIndex - 1);
if (objectTime > lastBreakEndTime && startTime < lastBreakEndTime) if (objectTime > lastBreakEndTime && startTime < lastBreakEndTime)
startTime = lastBreakEndTime; startTime = lastBreakEndTime;
} }
} }
if (autoXY == null) { if (autoXY == null) {
float[] xyEnd = hitObjects[objectIndex].getPointAt(trackPosition); float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime; int totalTime = objectTime - startTime;
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime); autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press // hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300]; int offset300 = hitResultOffset[GameData.HIT_300];
if ((osu.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) || if ((beatmap.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) ||
(osu.objects[objectIndex - 1].isCircle() && trackPosition - osu.objects[objectIndex - 1].getTime() < offset300)) (beatmap.objects[objectIndex - 1].isCircle() && trackPosition - beatmap.objects[objectIndex - 1].getTime() < offset300))
autoMousePressed = true; autoMousePressed = true;
} }
} else { } else {
autoXY = hitObjects[objectIndex].getPointAt(trackPosition); autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true; autoMousePressed = true;
} }
} else { } else {
// last object // last object
autoXY = hitObjects[objectIndex - 1].getPointAt(trackPosition); autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
} }
// set mouse coordinates // set mouse coordinates
@@ -388,12 +394,12 @@ public class Game extends BasicGameState {
} }
// break periods // break periods
if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) { if (beatmap.breaks != null && breakIndex < beatmap.breaks.size() && breakTime > 0) {
int endTime = osu.breaks.get(breakIndex); int endTime = beatmap.breaks.get(breakIndex);
int breakLength = endTime - breakTime; int breakLength = endTime - breakTime;
// letterbox effect (black bars on top/bottom) // letterbox effect (black bars on top/bottom)
if (osu.letterboxInBreaks && breakLength >= 4000) { if (beatmap.letterboxInBreaks && breakLength >= 4000) {
g.setColor(Color.black); g.setColor(Color.black);
g.fillRect(0, 0, width, height * 0.125f); g.fillRect(0, 0, width, height * 0.125f);
g.fillRect(0, height * 0.875f, 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 // skip beginning
if (objectIndex == 0 && if (objectIndex == 0 &&
trackPosition < osu.objects[0].getTime() - SKIP_OFFSET) trackPosition < beatmap.objects[0].getTime() - SKIP_OFFSET)
skipButton.draw(); skipButton.draw();
// show retries // show retries
@@ -465,40 +471,41 @@ public class Game extends BasicGameState {
trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in
// countdown // countdown
if (osu.countdown > 0) { // TODO: implement half/double rate settings if (beatmap.countdown > 0) {
float speedModifier = GameMod.getSpeedMultiplier() * playbackSpeed.getModifier();
timeDiff = firstObjectTime - trackPosition; timeDiff = firstObjectTime - trackPosition;
if (timeDiff >= 500 && timeDiff < 3000) { if (timeDiff >= 500 * speedModifier && timeDiff < 3000 * speedModifier) {
if (timeDiff >= 1500) { if (timeDiff >= 1500 * speedModifier) {
GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2); GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2);
if (!countdownReadySound) { if (!countdownReadySound) {
SoundController.playSound(SoundEffect.READY); SoundController.playSound(SoundEffect.READY);
countdownReadySound = true; countdownReadySound = true;
} }
} }
if (timeDiff < 2000) { if (timeDiff < 2000 * speedModifier) {
GameImage.COUNTDOWN_3.getImage().draw(0, 0); GameImage.COUNTDOWN_3.getImage().draw(0, 0);
if (!countdown3Sound) { if (!countdown3Sound) {
SoundController.playSound(SoundEffect.COUNT3); SoundController.playSound(SoundEffect.COUNT3);
countdown3Sound = true; countdown3Sound = true;
} }
} }
if (timeDiff < 1500) { if (timeDiff < 1500 * speedModifier) {
GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0); GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0);
if (!countdown2Sound) { if (!countdown2Sound) {
SoundController.playSound(SoundEffect.COUNT2); SoundController.playSound(SoundEffect.COUNT2);
countdown2Sound = true; countdown2Sound = true;
} }
} }
if (timeDiff < 1000) { if (timeDiff < 1000 * speedModifier) {
GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2); GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2);
if (!countdown1Sound) { if (!countdown1Sound) {
SoundController.playSound(SoundEffect.COUNT1); SoundController.playSound(SoundEffect.COUNT1);
countdown1Sound = true; countdown1Sound = true;
} }
} }
} else if (timeDiff >= -500 && timeDiff < 500) { } else if (timeDiff >= -500 * speedModifier && timeDiff < 500 * speedModifier) {
Image go = GameImage.COUNTDOWN_GO.getImage(); 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); go.drawCentered(width / 2, height / 2);
if (!countdownGoSound) { if (!countdownGoSound) {
SoundController.playSound(SoundEffect.GO); SoundController.playSound(SoundEffect.GO);
@@ -515,6 +522,10 @@ public class Game extends BasicGameState {
if (GameMod.AUTO.isActive()) if (GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); 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 // returning from pause screen
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) { if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
// darken the screen // darken the screen
@@ -539,7 +550,6 @@ public class Game extends BasicGameState {
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed()); UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
else else
UI.draw(g); UI.draw(g);
} }
@Override @Override
@@ -548,6 +558,8 @@ public class Game extends BasicGameState {
UI.update(delta); UI.update(delta);
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
skipButton.hoverUpdate(delta, mouseX, mouseY); skipButton.hoverUpdate(delta, mouseX, mouseY);
if (isReplay || GameMod.AUTO.isActive())
playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
// returning from pause screen: must click previous mouse position // returning from pause screen: must click previous mouse position
@@ -660,10 +672,10 @@ public class Game extends BasicGameState {
} }
// map complete! // 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 // track ended before last object was processed: force a hit result
if (MusicController.trackEnded() && objectIndex < hitObjects.length) if (MusicController.trackEnded() && objectIndex < gameObjects.length)
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition); gameObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
// if checkpoint used, skip ranking screen // if checkpoint used, skip ranking screen
if (checkpointLoaded) if (checkpointLoaded)
@@ -681,11 +693,11 @@ public class Game extends BasicGameState {
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1); replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0)); 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) if (r != null && !unranked)
r.save(); r.save();
} }
ScoreData score = data.getScoreData(osu); ScoreData score = data.getScoreData(beatmap);
// add score to database // add score to database
if (!unranked && !isReplay) if (!unranked && !isReplay)
@@ -697,23 +709,21 @@ public class Game extends BasicGameState {
} }
// timing points // timing points
if (timingPointIndex < osu.timingPoints.size()) { if (timingPointIndex < beatmap.timingPoints.size()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex); TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
if (trackPosition >= timingPoint.getTime()) { if (trackPosition >= timingPoint.getTime()) {
setBeatLength(timingPoint); setBeatLength(timingPoint, true);
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
SoundController.setSampleVolume(timingPoint.getSampleVolume());
timingPointIndex++; timingPointIndex++;
} }
} }
// song beginning // song beginning
if (objectIndex == 0 && trackPosition < osu.objects[0].getTime()) if (objectIndex == 0 && trackPosition < beatmap.objects[0].getTime())
return; // nothing to do here return; // nothing to do here
// break periods // break periods
if (osu.breaks != null && breakIndex < osu.breaks.size()) { if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
int breakValue = osu.breaks.get(breakIndex); int breakValue = beatmap.breaks.get(breakIndex);
if (breakTime > 0) { // in a break period if (breakTime > 0) { // in a break period
if (trackPosition < breakValue) if (trackPosition < breakValue)
return; return;
@@ -765,13 +775,13 @@ public class Game extends BasicGameState {
// update objects (loop in unlikely event of any skipped indexes) // update objects (loop in unlikely event of any skipped indexes)
boolean keyPressed = keys != ReplayFrame.KEY_NONE; 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 // check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < hitObjects.length && boolean overlap = (objectIndex + 1 < hitObjects.length &&
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]); trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]);
// update hit object and check completion status // 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 objectIndex++; // done, so increment object index
else else
break; break;
@@ -807,7 +817,7 @@ public class Game extends BasicGameState {
} }
// pause game // pause game
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) { if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = mouseX; pausedMouseX = mouseX;
pausedMouseY = mouseY; pausedMouseY = mouseY;
pausePulse = 0f; pausePulse = 0f;
@@ -824,7 +834,7 @@ public class Game extends BasicGameState {
// restart // restart
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) { if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
try { try {
if (trackPosition < osu.objects[0].getTime()) if (trackPosition < beatmap.objects[0].getTime())
retries--; // don't count this retry (cancel out later increment) retries--; // don't count this retry (cancel out later increment)
restart = Restart.MANUAL; restart = Restart.MANUAL;
enter(container, game); enter(container, game);
@@ -851,7 +861,7 @@ public class Game extends BasicGameState {
// load checkpoint // load checkpoint
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) { if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
int checkpoint = Options.getCheckpoint(); int checkpoint = Options.getCheckpoint();
if (checkpoint == 0 || checkpoint > osu.endTime) if (checkpoint == 0 || checkpoint > beatmap.endTime)
break; // invalid checkpoint break; // invalid checkpoint
try { try {
restart = Restart.MANUAL; restart = Restart.MANUAL;
@@ -866,11 +876,12 @@ public class Game extends BasicGameState {
// skip to checkpoint // skip to checkpoint
MusicController.setPosition(checkpoint); MusicController.setPosition(checkpoint);
while (objectIndex < hitObjects.length && MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
osu.objects[objectIndex++].getTime() <= checkpoint) while (objectIndex < gameObjects.length &&
beatmap.objects[objectIndex++].getTime() <= checkpoint)
; ;
objectIndex--; objectIndex--;
lastReplayTime = osu.objects[objectIndex].getTime(); lastReplayTime = beatmap.objects[objectIndex].getTime();
} catch (SlickException e) { } catch (SlickException e) {
ErrorHandler.error("Failed to load checkpoint.", e, false); ErrorHandler.error("Failed to load checkpoint.", e, false);
} }
@@ -896,13 +907,13 @@ public class Game extends BasicGameState {
@Override @Override
public void mousePressed(int button, int x, int y) { public void mousePressed(int button, int x, int y) {
if (Options.isMouseDisabled())
return;
// watching replay // watching replay
if (isReplay) { if (isReplay || GameMod.AUTO.isActive()) {
// only allow skip button if (button == Input.MOUSE_MIDDLE_BUTTON)
if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) return;
// skip button
if (skipButton.contains(x, y))
skipIntro(); skipIntro();
if(y < 50){ if(y < 50){
float pos = (float)x / width * osu.endTime; float pos = (float)x / width * osu.endTime;
@@ -912,10 +923,13 @@ public class Game extends BasicGameState {
return; return;
} }
if (Options.isMouseDisabled())
return;
// mouse wheel: pause the game // mouse wheel: pause the game
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) { if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition(); 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; pausedMouseX = x;
pausedMouseY = y; pausedMouseY = y;
pausePulse = 0f; pausePulse = 0f;
@@ -1031,8 +1045,11 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
UI.enter(); UI.enter();
if (osu == null || osu.objects == null) if (beatmap == null || beatmap.objects == null)
throw new RuntimeException("Running game with no OsuFile loaded."); 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) // grab the mouse (not working for touchscreen)
// container.setMouseGrabbed(true); // container.setMouseGrabbed(true);
@@ -1053,10 +1070,14 @@ public class Game extends BasicGameState {
// reset game data // reset game data
resetGameData(); resetGameData();
// needs to play before setting position to resume without lag later // load the first timingPoint for stacking
MusicController.play(false); if (!beatmap.timingPoints.isEmpty()) {
MusicController.setPosition(0); TimingPoint timingPoint = beatmap.timingPoints.get(0);
MusicController.pause(); if (!timingPoint.isInherited()) {
setBeatLength(timingPoint, true);
timingPointIndex++;
}
}
if (!osu.timingPoints.isEmpty()) { if (!osu.timingPoints.isEmpty()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(0); OsuTimingPoint timingPoint = osu.timingPoints.get(0);
@@ -1067,39 +1088,39 @@ public class Game extends BasicGameState {
hitObjects = new HitObject[osu.objects.length]; hitObjects = new HitObject[osu.objects.length];
// initialize object maps // initialize object maps
for (int i = 0; i < osu.objects.length; i++) { Color[] combo = beatmap.getComboColors();
OsuHitObject hitObject = osu.objects[i]; for (int i = 0; i < beatmap.objects.length; i++) {
HitObject hitObject = beatmap.objects[i];
// is this the last note in the combo? // is this the last note in the combo?
boolean comboEnd = false; boolean comboEnd = false;
if (i + 1 >= osu.objects.length || osu.objects[i + 1].isNewCombo()) if (i + 1 >= osu.objects.length || osu.objects[i + 1].isNewCombo())
comboEnd = true; comboEnd = true;
Color color = osu.combo[hitObject.getComboIndex()]; Color color = combo[hitObject.getComboIndex()];
// pass beatLength to hit objects // pass beatLength to hit objects
int hitObjectTime = hitObject.getTime(); int hitObjectTime = hitObject.getTime();
int timingPointIndex = 0; while (timingPointIndex < beatmap.timingPoints.size()) {
while (timingPointIndex < osu.timingPoints.size()) { TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
if (timingPoint.getTime() > hitObjectTime) if (timingPoint.getTime() > hitObjectTime)
break; break;
setBeatLength(timingPoint); setBeatLength(timingPoint, false);
timingPointIndex++; timingPointIndex++;
} }
try { try {
if (hitObject.isCircle()) 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()) 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()) else if (hitObject.isSpinner())
hitObjects[i] = new Spinner(hitObject, this, data); gameObjects[i] = new Spinner(hitObject, this, data);
} catch (Exception e) { } 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", ErrorHandler.error(String.format("Failed to create %s at index %d:\n%s",
hitObject.getTypeName(), i, hitObject.toString()), e, true); hitObject.getTypeName(), i, hitObject.toString()), e, true);
hitObjects[i] = new DummyObject(hitObject); gameObjects[i] = new DummyObject(hitObject);
continue; continue;
} }
} }
@@ -1108,19 +1129,19 @@ public class Game extends BasicGameState {
calculateStacks(); calculateStacks();
// load the first timingPoint // load the first timingPoint
if (!osu.timingPoints.isEmpty()) { timingPointIndex = 0;
OsuTimingPoint timingPoint = osu.timingPoints.get(0); beatLengthBase = beatLength = 1;
if (!beatmap.timingPoints.isEmpty()) {
TimingPoint timingPoint = beatmap.timingPoints.get(0);
if (!timingPoint.isInherited()) { if (!timingPoint.isInherited()) {
beatLengthBase = beatLength = timingPoint.getBeatLength(); setBeatLength(timingPoint, true);
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
SoundController.setSampleVolume(timingPoint.getSampleVolume());
timingPointIndex++; timingPointIndex++;
} }
} }
// unhide cursor for "auto" mod and replays // unhide cursor for "auto" mod and replays
if (GameMod.AUTO.isActive() || isReplay) if (GameMod.AUTO.isActive() || isReplay)
UI.showCursor(); UI.getCursor().show();
// load replay frames // load replay frames
if (isReplay) { if (isReplay) {
@@ -1156,11 +1177,20 @@ public class Game extends BasicGameState {
replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0)); replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0));
} }
leadInTime = osu.audioLeadIn + approachTime; leadInTime = beatmap.audioLeadIn + approachTime;
restart = Restart.FALSE; 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(); skipButton.resetHover();
if (isReplay || GameMod.AUTO.isActive())
playbackSpeed.getButton().resetHover();
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
} }
@Override @Override
@@ -1170,11 +1200,14 @@ public class Game extends BasicGameState {
// re-hide cursor // re-hide cursor
if (GameMod.AUTO.isActive() || isReplay) if (GameMod.AUTO.isActive() || isReplay)
UI.hideCursor(); UI.getCursor().hide();
// replays // replays
if (isReplay) if (isReplay)
GameMod.loadModState(previousMods); 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) { private void drawHitObjects(Graphics g, int trackPosition) {
// include previous object in follow points // include previous object in follow points
int lastObjectIndex = -1; int lastObjectIndex = -1;
if (objectIndex > 0 && objectIndex < osu.objects.length && if (objectIndex > 0 && objectIndex < beatmap.objects.length &&
trackPosition < osu.objects[objectIndex].getTime() && !osu.objects[objectIndex - 1].isSpinner()) trackPosition < beatmap.objects[objectIndex].getTime() && !beatmap.objects[objectIndex - 1].isSpinner())
lastObjectIndex = objectIndex - 1; lastObjectIndex = objectIndex - 1;
// draw hit objects in reverse order, or else overlapping objects are unreadable // draw hit objects in reverse order, or else overlapping objects are unreadable
Stack<Integer> stack = new Stack<Integer>(); Stack<Integer> stack = new Stack<Integer>();
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); stack.add(index);
// draw follow points // draw follow points
if (!Options.isFollowPointEnabled()) if (!Options.isFollowPointEnabled())
continue; continue;
if (osu.objects[index].isSpinner()) { if (beatmap.objects[index].isSpinner()) {
lastObjectIndex = -1; lastObjectIndex = -1;
continue; continue;
} }
if (lastObjectIndex != -1 && !osu.objects[index].isNewCombo()) { if (lastObjectIndex != -1 && !beatmap.objects[index].isNewCombo()) {
// calculate points // calculate points
final int followPointInterval = container.getHeight() / 14; final int followPointInterval = container.getHeight() / 14;
int lastObjectEndTime = hitObjects[lastObjectIndex].getEndTime() + 1; int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
int objectStartTime = osu.objects[index].getTime(); int objectStartTime = beatmap.objects[index].getTime();
float[] startXY = hitObjects[lastObjectIndex].getPointAt(lastObjectEndTime); float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
float[] endXY = hitObjects[index].getPointAt(objectStartTime); float[] endXY = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endXY[0] - startXY[0]; float xDiff = endXY[0] - startXY[0];
float yDiff = endXY[1] - startXY[1]; float yDiff = endXY[1] - startXY[1];
float dist = (float) Math.hypot(xDiff, yDiff); float dist = (float) Math.hypot(xDiff, yDiff);
@@ -1254,29 +1287,32 @@ public class Game extends BasicGameState {
} }
while (!stack.isEmpty()) while (!stack.isEmpty())
hitObjects[stack.pop()].draw(g, trackPosition); gameObjects[stack.pop()].draw(g, trackPosition);
// draw OsuHitObjectResult objects // draw OsuHitObjectResult objects
data.drawHitResults(trackPosition); data.drawHitResults(trackPosition);
} }
/** /**
* Loads all required data from an OsuFile. * Loads all required data from a beatmap.
* @param osu the OsuFile to load * @param beatmap the beatmap to load
*/ */
public void loadOsuFile(OsuFile osu) { public void loadBeatmap(Beatmap beatmap) {
this.osu = osu; this.beatmap = beatmap;
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString())); Display.setTitle(String.format("%s - %s", game.getTitle(), beatmap.toString()));
if (osu.timingPoints == null || osu.combo == null) if (beatmap.timingPoints == null)
OsuDB.load(osu, OsuDB.LOAD_ARRAY); BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
OsuParser.parseHitObjects(osu); BeatmapParser.parseHitObjects(beatmap);
HitSound.setDefaultSampleSet(osu.sampleSet); HitSound.setDefaultSampleSet(beatmap.sampleSet);
} }
/** /**
* Resets all game data and structures. * Resets all game data and structures.
*/ */
public void resetGameData() { public void resetGameData() {
//conflict
gameObjects = new GameObject[beatmap.objects.length];
//
data.clear(); data.clear();
objectIndex = 0; objectIndex = 0;
breakIndex = 0; breakIndex = 0;
@@ -1301,16 +1337,17 @@ public class Game extends BasicGameState {
autoMouseY = 0; autoMouseY = 0;
autoMousePressed = false; autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3; flashlightRadius = container.getHeight() * 2 / 3;
playbackSpeed = PlaybackSpeed.NORMAL;
System.gc(); System.gc();
} }
/** /**
* Skips the beginning of a track. * Skips the beginning of a track.
* @return true if skipped, false otherwise * @return {@code true} if skipped, {@code false} otherwise
*/ */
private synchronized boolean skipIntro() { private synchronized boolean skipIntro() {
int firstObjectTime = osu.objects[0].getTime(); int firstObjectTime = beatmap.objects[0].getTime();
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) { if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
if (isLeadIn()) { if (isLeadIn()) {
@@ -1318,6 +1355,7 @@ public class Game extends BasicGameState {
MusicController.resume(); MusicController.resume();
} }
MusicController.setPosition(firstObjectTime - SKIP_OFFSET); MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
replaySkipTime = (isReplay) ? -1 : trackPosition; replaySkipTime = (isReplay) ? -1 : trackPosition;
if (isReplay) { if (isReplay) {
replayX = (int) skipButton.getX(); replayX = (int) skipButton.getX();
@@ -1337,7 +1375,7 @@ public class Game extends BasicGameState {
int height = container.getHeight(); int height = container.getHeight();
// set images // set images
File parent = osu.getFile().getParentFile(); File parent = beatmap.getFile().getParentFile();
for (GameImage img : GameImage.values()) { for (GameImage img : GameImage.values()) {
if (img.isSkinnable()) { if (img.isSkinnable()) {
img.setDefaultImage(); img.setDefaultImage();
@@ -1365,26 +1403,11 @@ public class Game extends BasicGameState {
*/ */
private void setMapModifiers() { private void setMapModifiers() {
// map-based properties, re-initialized each game // map-based properties, re-initialized each game
float circleSize = osu.circleSize; float multiplier = GameMod.getDifficultyMultiplier();
float approachRate = osu.approachRate; float circleSize = Math.min(beatmap.circleSize * multiplier, 10f);
float overallDifficulty = osu.overallDifficulty; float approachRate = Math.min(beatmap.approachRate * multiplier, 10f);
float HPDrainRate = osu.HPDrainRate; float overallDifficulty = Math.min(beatmap.overallDifficulty * multiplier, 10f);
float HPDrainRate = Math.min(beatmap.HPDrainRate * multiplier, 10f);
// "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;
}
// fixed difficulty overrides // fixed difficulty overrides
if (Options.getFixedCS() > 0f) if (Options.getFixedCS() > 0f)
@@ -1399,12 +1422,14 @@ public class Game extends BasicGameState {
// Stack modifier scales with hit object size // Stack modifier scales with hit object size
// StackOffset = HitObjectRadius / 10 // StackOffset = HitObjectRadius / 10
int diameter = (int) (104 - (circleSize * 8)); int diameter = (int) (104 - (circleSize * 8));
OsuHitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER); HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
// initialize objects // initialize objects
Circle.init(container, circleSize); Circle.init(container, circleSize);
Slider.init(container, circleSize, osu); Slider.init(container, circleSize, beatmap);
Spinner.init(container); Spinner.init(container);
Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
// approachRate (hit object approach time) // approachRate (hit object approach time)
if (approachRate < 5) 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; } public void setRestart(Restart restart) { this.restart = restart; }
/**
* Returns the current restart state.
*/
public Restart getRestart() { return restart; } 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. * 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()) if (!timingPoint.isInherited())
beatLengthBase = beatLength = timingPoint.getBeatLength(); beatLengthBase = beatLength = timingPoint.getBeatLength();
else else
beatLength = beatLengthBase * timingPoint.getSliderMultiplier(); 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) { private void sendGameKeyPress(int keys, int x, int y, int trackPosition) {
System.out.println("Game Key Pressed"+keys+" "+x+" "+y+" "+objectIndex); 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; return;
OsuHitObject hitObject = osu.objects[objectIndex]; HitObject hitObject = beatmap.objects[objectIndex];
// circles // circles
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition)) if (hitObject.isCircle() && gameObjects[objectIndex].mousePressed(x, y, trackPosition))
objectIndex++; // circle hit objectIndex++; // circle hit
// sliders // sliders
else if (hitObject.isSlider()) 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) { private ReplayFrame addReplayFrame(int x, int y, int keys, int time) {
int timeDiff = time - lastReplayTime; int timeDiff = time - lastReplayTime;
lastReplayTime = time; lastReplayTime = time;
int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier()); int cx = (int) ((x - HitObject.getXOffset()) / HitObject.getXMultiplier());
int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier()); int cy = (int) ((y - HitObject.getYOffset()) / HitObject.getYMultiplier());
ReplayFrame frame = new ReplayFrame(timeDiff, time, cx, cy, keys); ReplayFrame frame = new ReplayFrame(timeDiff, time, cx, cy, keys);
if (replayFrames != null) if (replayFrames != null)
replayFrames.add(frame); replayFrames.add(frame);
@@ -1603,14 +1639,14 @@ public class Game extends BasicGameState {
return; return;
int width = container.getWidth(), height = container.getHeight(); 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()) { if (isLeadIn()) {
// lead-in: expand area // 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); flashlightRadius = width - (int) ((width - (height * 2 / 3)) * progress);
} else if (firstObject) { } else if (firstObject) {
// before first object: shrink area // before first object: shrink area
int timeDiff = osu.objects[0].getTime() - trackPosition; int timeDiff = beatmap.objects[0].getTime() - trackPosition;
flashlightRadius = width; flashlightRadius = width;
if (timeDiff < approachTime) { if (timeDiff < approachTime) {
float progress = (float) timeDiff / approachTime; float progress = (float) timeDiff / approachTime;
@@ -1626,10 +1662,10 @@ public class Game extends BasicGameState {
targetRadius = height / 2; targetRadius = height / 2;
else else
targetRadius = height / 3; 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 // breaks: expand at beginning, shrink at end
flashlightRadius = targetRadius; flashlightRadius = targetRadius;
int endTime = osu.breaks.get(breakIndex); int endTime = beatmap.breaks.get(breakIndex);
int breakLength = endTime - breakTime; int breakLength = endTime - breakTime;
if (breakLength > approachTime * 3) { if (breakLength > approachTime * 3) {
float progress = 1f; float progress = 1f;
@@ -1662,8 +1698,8 @@ public class Game extends BasicGameState {
*/ */
private void calculateStacks() { private void calculateStacks() {
// reverse pass for stack calculation // reverse pass for stack calculation
for (int i = hitObjects.length - 1; i > 0; i--) { for (int i = gameObjects.length - 1; i > 0; i--) {
OsuHitObject hitObjectI = osu.objects[i]; HitObject hitObjectI = beatmap.objects[i];
// already calculated // already calculated
if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner()) if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner())
@@ -1671,33 +1707,33 @@ public class Game extends BasicGameState {
// search for hit objects in stack // search for hit objects in stack
for (int n = i - 1; n >= 0; n--) { for (int n = i - 1; n >= 0; n--) {
OsuHitObject hitObjectN = osu.objects[n]; HitObject hitObjectN = beatmap.objects[n];
if (hitObjectN.isSpinner()) if (hitObjectN.isSpinner())
continue; continue;
// check if in range stack calculation // check if in range stack calculation
float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * osu.stackLeniency); float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * beatmap.stackLeniency);
float timeN = hitObjectN.isSlider() ? hitObjects[n].getEndTime() : hitObjectN.getTime(); float timeN = hitObjectN.isSlider() ? gameObjects[n].getEndTime() : hitObjectN.getTime();
if (timeI > timeN) if (timeI > timeN)
break; break;
// possible special case: if slider end in the stack, // possible special case: if slider end in the stack,
// all next hit objects in stack move right down // all next hit objects in stack move right down
if (hitObjectN.isSlider()) { if (hitObjectN.isSlider()) {
float[] p1 = hitObjects[i].getPointAt(hitObjectI.getTime()); float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
float[] p2 = hitObjects[n].getPointAt(hitObjects[n].getEndTime()); float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]); float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
// check if hit object part of this stack // 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; int offset = hitObjectI.getStack() - hitObjectN.getStack() + 1;
for (int j = n + 1; j <= i; j++) { for (int j = n + 1; j <= i; j++) {
OsuHitObject hitObjectJ = osu.objects[j]; HitObject hitObjectJ = beatmap.objects[j];
p1 = hitObjects[j].getPointAt(hitObjectJ.getTime()); p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]); distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
// hit object below slider end // hit object below slider end
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
hitObjectJ.setStack(hitObjectJ.getStack() - offset); hitObjectJ.setStack(hitObjectJ.getStack() - offset);
} }
break; // slider end always start of the stack: reset calculation break; // slider end always start of the stack: reset calculation
@@ -1717,9 +1753,9 @@ public class Game extends BasicGameState {
} }
// update hit object positions // update hit object positions
for (int i = 0; i < hitObjects.length; i++) { for (int i = 0; i < gameObjects.length; i++) {
if (osu.objects[i].getStack() != 0) if (beatmap.objects[i].getStack() != 0)
hitObjects[i].updatePosition(); gameObjects[i].updatePosition();
} }
} }
} }

View File

@@ -19,14 +19,14 @@
package itdelatrisu.opsu.states; package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
@@ -132,8 +132,9 @@ public class GamePauseMenu extends BasicGameState {
if (gameState.getRestart() == Game.Restart.LOSE) { if (gameState.getRestart() == Game.Restart.LOSE) {
SoundController.playSound(SoundEffect.MENUBACK); SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad(); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
MusicController.playAt(MusicController.getOsuFile().previewTime, true); MusicController.playAt(MusicController.getBeatmap().previewTime, true);
UI.resetCursor(); if (UI.getCursor().isSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else { } else {
SoundController.playSound(SoundEffect.MENUBACK); SoundController.playSound(SoundEffect.MENUBACK);
@@ -183,10 +184,11 @@ public class GamePauseMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUBACK); SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad(); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
if (loseState) if (loseState)
MusicController.playAt(MusicController.getOsuFile().previewTime, true); MusicController.playAt(MusicController.getBeatmap().previewTime, true);
else else
MusicController.resume(); MusicController.resume();
UI.resetCursor(); if (UI.getCursor().isSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} }
} }

View File

@@ -20,16 +20,16 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -102,14 +102,14 @@ public class GameRanking extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
OsuFile osu = MusicController.getOsuFile(); Beatmap beatmap = MusicController.getBeatmap();
// background // background
if (!osu.drawBG(width, height, 0.7f, true)) if (!beatmap.drawBG(width, height, 0.7f, true))
GameImage.PLAYFIELD.getImage().draw(0,0); GameImage.PLAYFIELD.getImage().draw(0,0);
// ranking screen elements // ranking screen elements
data.drawRankingElements(g, osu); data.drawRankingElements(g, beatmap);
// buttons // buttons
replayButton.draw(); replayButton.draw();
@@ -201,8 +201,8 @@ public class GameRanking extends BasicGameState {
} }
if (returnToGame) { if (returnToGame) {
OsuFile osu = MusicController.getOsuFile(); Beatmap beatmap = MusicController.getBeatmap();
gameState.loadOsuFile(osu); gameState.loadBeatmap(beatmap);
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return; return;
@@ -244,7 +244,8 @@ public class GameRanking extends BasicGameState {
songMenu.resetGameDataOnLoad(); songMenu.resetGameDataOnLoad();
songMenu.resetTrackOnLoad(); songMenu.resetTrackOnLoad();
} }
UI.resetCursor(); if (UI.getCursor().isSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} }

View File

@@ -20,20 +20,20 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.MenuButton.Expand;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; 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.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.MenuButton.Expand;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.IOException; import java.io.IOException;
@@ -91,7 +91,7 @@ public class MainMenu extends BasicGameState {
private MenuButton updateButton; private MenuButton updateButton;
/** Application start time, for drawing the total running time. */ /** Application start time, for drawing the total running time. */
private long osuStartTime; private long programStartTime;
/** Indexes of previous songs. */ /** Indexes of previous songs. */
private Stack<Integer> previous; private Stack<Integer> previous;
@@ -127,7 +127,7 @@ public class MainMenu extends BasicGameState {
this.game = game; this.game = game;
this.input = container.getInput(); this.input = container.getInput();
osuStartTime = System.currentTimeMillis(); programStartTime = System.currentTimeMillis();
previous = new Stack<Integer>(); previous = new Stack<Integer>();
int width = container.getWidth(); int width = container.getWidth();
@@ -199,9 +199,9 @@ public class MainMenu extends BasicGameState {
int height = container.getHeight(); int height = container.getHeight();
// draw background // draw background
OsuFile osu = MusicController.getOsuFile(); Beatmap beatmap = MusicController.getBeatmap();
if (Options.isDynamicBackgroundEnabled() && if (Options.isDynamicBackgroundEnabled() &&
osu != null && osu.drawBG(width, height, bgAlpha, true)) beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
; ;
else { else {
Image bg = GameImage.MENU_BG.getImage(); 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.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4); g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white); g.setColor(Color.white);
if (!MusicController.isTrackLoading() && osu != null) { if (!MusicController.isTrackLoading() && beatmap != null) {
float musicBarPosition = Math.min((float) MusicController.getPosition() / MusicController.getDuration(), 1f); float musicBarPosition = Math.min((float) MusicController.getPosition() / MusicController.getDuration(), 1f);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4); g.fillRoundRect(musicBarX, musicBarY, musicBarWidth * musicBarPosition, musicBarHeight, 4);
} }
@@ -270,25 +270,25 @@ public class MainMenu extends BasicGameState {
} }
// draw text // 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); 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.", 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()) if (MusicController.isTrackLoading())
g.drawString("Track loading...", marginX, marginY + lineHeight); g.drawString("Track loading...", marginX, topMarginY + lineHeight);
else if (MusicController.trackExists()) { else if (MusicController.trackExists()) {
if (Options.useUnicodeMetadata()) // load glyphs if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_MEDIUM, osu.titleUnicode, osu.artistUnicode); Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, marginY + lineHeight); g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
g.drawString(String.format("%s: %s", osu.getArtist(), osu.getTitle()), marginX + 25, marginY + (lineHeight * 2)); 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.", g.drawString(String.format("opsu! has been running for %s.",
Utils.getTimeString((int) (System.currentTimeMillis() - osuStartTime) / 1000)), Utils.getTimeString((int) (System.currentTimeMillis() - programStartTime) / 1000)),
marginX, height - marginY - (lineHeight * 2)); marginX, height - bottomMarginY - (lineHeight * 2));
g.drawString(String.format("It is currently %s.", g.drawString(String.format("It is currently %s.",
new SimpleDateFormat("h:mm a").format(new Date())), new SimpleDateFormat("h:mm a").format(new Date())),
marginX, height - marginY - lineHeight); marginX, height - bottomMarginY - lineHeight);
UI.draw(g); UI.draw(g);
} }
@@ -455,7 +455,7 @@ public class MainMenu extends BasicGameState {
} else if (musicPrevious.contains(x, y)) { } else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) { if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); 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()) if (Options.isDynamicBackgroundEnabled())
bgAlpha = 0f; bgAlpha = 0f;
} else } else
@@ -603,10 +603,10 @@ public class MainMenu extends BasicGameState {
private void nextTrack() { private void nextTrack() {
boolean isTheme = MusicController.isThemePlaying(); boolean isTheme = MusicController.isThemePlaying();
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); 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; boolean sameAudio = false;
if (node != null) { 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) if (!isTheme && !sameAudio)
previous.add(node.index); previous.add(node.index);
} }
@@ -619,7 +619,7 @@ public class MainMenu extends BasicGameState {
*/ */
private void enterSongMenu() { private void enterSongMenu() {
int state = Opsu.STATE_SONGMENU; 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!"); ((DownloadsMenu) game.getState(Opsu.STATE_DOWNLOADSMENU)).notifyOnLoad("Download some beatmaps to get started!");
state = Opsu.STATE_DOWNLOADSMENU; state = Opsu.STATE_DOWNLOADSMENU;
} }

View File

@@ -19,15 +19,15 @@
package itdelatrisu.opsu.states; package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Options.GameOption; import itdelatrisu.opsu.Options.GameOption;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -52,12 +52,14 @@ public class OptionsMenu extends BasicGameState {
DISPLAY ("Display", new GameOption[] { DISPLAY ("Display", new GameOption[] {
GameOption.SCREEN_RESOLUTION, GameOption.SCREEN_RESOLUTION,
// GameOption.FULLSCREEN, // GameOption.FULLSCREEN,
GameOption.SKIN,
GameOption.TARGET_FPS, GameOption.TARGET_FPS,
GameOption.SHOW_FPS, GameOption.SHOW_FPS,
GameOption.SHOW_UNICODE, GameOption.SHOW_UNICODE,
GameOption.SCREENSHOT_FORMAT, GameOption.SCREENSHOT_FORMAT,
GameOption.NEW_CURSOR, GameOption.NEW_CURSOR,
GameOption.DYNAMIC_BACKGROUND, GameOption.DYNAMIC_BACKGROUND,
GameOption.LOAD_HD_IMAGES,
GameOption.LOAD_VERBOSE GameOption.LOAD_VERBOSE
}), }),
MUSIC ("Music", new GameOption[] { MUSIC ("Music", new GameOption[] {
@@ -90,8 +92,7 @@ public class OptionsMenu extends BasicGameState {
GameOption.FIXED_HP, GameOption.FIXED_HP,
GameOption.FIXED_AR, GameOption.FIXED_AR,
GameOption.FIXED_OD, GameOption.FIXED_OD,
GameOption.CHECKPOINT, GameOption.CHECKPOINT
GameOption.LOAD_HD_IMAGES
}); });
/** Total number of tabs. */ /** Total number of tabs. */
@@ -180,9 +181,9 @@ public class OptionsMenu extends BasicGameState {
// option tabs // option tabs
Image tabImage = GameImage.MENU_TAB.getImage(); Image tabImage = GameImage.MENU_TAB.getImage();
float tabX = (width / 50) + (tabImage.getWidth() / 2f); float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
float tabY = Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() + float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
height * 0.03f + (tabImage.getHeight() / 2f); height * 0.015f - (tabImage.getHeight() / 2f);
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE); int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
for (OptionTab tab : OptionTab.values()) for (OptionTab tab : OptionTab.values())
tab.button = new MenuButton(tabImage, tabX + (tab.ordinal() * tabOffset), tabY); tab.button = new MenuButton(tabImage, tabX + (tab.ordinal() * tabOffset), tabY);
@@ -201,12 +202,16 @@ public class OptionsMenu extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
// title // title
float c = container.getWidth() * 0.02f; float marginX = width * 0.015f, marginY = height * 0.01f;
Utils.FONT_LARGE.drawString(c, c, "Game Options", Color.white); Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
Utils.FONT_DEFAULT.drawString(c, c + Utils.FONT_LARGE.getLineHeight() * 0.9f, Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
"Click or drag an option to change it.", Color.white); "Change the way opsu! behaves", Color.white);
// background
GameImage.OPTIONS_BG.getImage().draw(0, lineY);
// game options // game options
g.setLineWidth(1f); g.setLineWidth(1f);
@@ -235,7 +240,6 @@ public class OptionsMenu extends BasicGameState {
currentTab.getName(), true, false); currentTab.getName(), true, false);
g.setColor(Color.white); g.setColor(Color.white);
g.setLineWidth(2f); g.setLineWidth(2f);
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
g.drawLine(0, lineY, width, lineY); g.drawLine(0, lineY, width, lineY);
g.resetLineWidth(); g.resetLineWidth();
@@ -302,7 +306,7 @@ public class OptionsMenu extends BasicGameState {
// options (click only) // options (click only)
GameOption option = getOptionAt(y); GameOption option = getOptionAt(y);
if (option != GameOption.NULL) if (option != null)
option.click(container); option.click(container);
// special key entry states // special key entry states
@@ -338,7 +342,7 @@ public class OptionsMenu extends BasicGameState {
// options (drag only) // options (drag only)
GameOption option = getOptionAt(oldy); GameOption option = getOptionAt(oldy);
if (option != GameOption.NULL) if (option != null)
option.drag(container, diff); option.drag(container, diff);
} }
@@ -425,14 +429,13 @@ public class OptionsMenu extends BasicGameState {
* @return the option, or GameOption.NULL if no such option exists * @return the option, or GameOption.NULL if no such option exists
*/ */
private GameOption getOptionAt(int y) { private GameOption getOptionAt(int y) {
GameOption option = GameOption.NULL;
if (y < textY || y > textY + (offsetY * maxOptionsScreen)) if (y < textY || y > textY + (offsetY * maxOptionsScreen))
return option; return null;
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY; int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
if (index < currentTab.options.length) if (index >= currentTab.options.length)
option = currentTab.options[index]; return null;
return option;
return currentTab.options[index];
} }
} }

View File

@@ -22,25 +22,25 @@ import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameData.Grade; import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.MenuButton;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; 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.OszUnpacker;
import itdelatrisu.opsu.ScoreData; import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.SongSort;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MultiClip; import itdelatrisu.opsu.audio.MultiClip;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.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.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
import java.util.Map; import java.util.Map;
@@ -91,28 +91,28 @@ public class SongMenu extends BasicGameState {
/** Line width of the header/footer divider. */ /** Line width of the header/footer divider. */
private static final int DIVIDER_LINE_WIDTH = 4; 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 { private static class SongNode {
/** Song node. */ /** Song node. */
private OsuGroupNode node; private BeatmapSetNode node;
/** File index. */ /** File index. */
private int index; private int index;
/** /**
* Constructor. * Constructor.
* @param node the OsuGroupNode * @param node the BeatmapSetNode
* @param index the file index * @param index the file index
*/ */
public SongNode(OsuGroupNode node, int index) { public SongNode(BeatmapSetNode node, int index) {
this.node = node; this.node = node;
this.index = index; 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. * Returns the associated file index.
@@ -121,10 +121,10 @@ public class SongMenu extends BasicGameState {
} }
/** Current start node (topmost menu entry). */ /** Current start node (topmost menu entry). */
private OsuGroupNode startNode; private BeatmapSetNode startNode;
/** Current focused (selected) node. */ /** Current focused (selected) node. */
private OsuGroupNode focusNode; private BeatmapSetNode focusNode;
/** The base node of the previous focus node. */ /** The base node of the previous focus node. */
private SongNode oldFocusNode = null; private SongNode oldFocusNode = null;
@@ -172,7 +172,7 @@ public class SongMenu extends BasicGameState {
private MenuState stateAction; private MenuState stateAction;
/** If non-null, the node that stateAction acts upon. */ /** 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. */ /** If non-null, the score data that stateAction acts upon. */
private ScoreData stateActionScore; private ScoreData stateActionScore;
@@ -228,7 +228,7 @@ public class SongMenu extends BasicGameState {
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight(); footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
// initialize sorts // initialize sorts
for (SongSort sort : SongSort.values()) for (BeatmapSortOrder sort : BeatmapSortOrder.values())
sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2); sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2);
// initialize score data buttons // initialize score data buttons
@@ -292,13 +292,13 @@ public class SongMenu extends BasicGameState {
// background // background
if (focusNode != null) { if (focusNode != null) {
OsuFile focusNodeOsu = focusNode.osuFiles.get(focusNode.osuFileIndex); Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
if (!focusNodeOsu.drawBG(width, height, 1.0f, true)) if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
GameImage.PLAYFIELD.getImage().draw(); GameImage.PLAYFIELD.getImage().draw();
} }
// song buttons // song buttons
OsuGroupNode node = startNode; BeatmapSetNode node = startNode;
int songButtonIndex = 0; int songButtonIndex = 0;
if (node != null && node.prev != null) { if (node != null && node.prev != null) {
node = node.prev; node = node.prev;
@@ -339,21 +339,27 @@ public class SongMenu extends BasicGameState {
if (songInfo == null) { if (songInfo == null) {
songInfo = focusNode.getInfo(); songInfo = focusNode.getInfo();
if (Options.useUnicodeMetadata()) { // load glyphs if (Options.useUnicodeMetadata()) { // load glyphs
OsuFile osu = focusNode.osuFiles.get(0); Beatmap beatmap = focusNode.getBeatmapSet().get(0);
Utils.loadGlyphs(Utils.FONT_LARGE, osu.titleUnicode, osu.artistUnicode); Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
} }
} }
marginX += 5; marginX += 5;
float headerTextY = marginY; float headerTextY = marginY * 0.2f;
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white); 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); Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2; 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; headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white); Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4; 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 // score buttons
@@ -382,15 +388,15 @@ public class SongMenu extends BasicGameState {
selectOptionsButton.draw(); selectOptionsButton.draw();
// sorting tabs // sorting tabs
SongSort currentSort = SongSort.getSort(); BeatmapSortOrder currentSort = BeatmapSortOrder.getSort();
SongSort hoverSort = null; BeatmapSortOrder hoverSort = null;
for (SongSort sort : SongSort.values()) { for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
if (sort.contains(mouseX, mouseY)) { if (sort.contains(mouseX, mouseY)) {
hoverSort = sort; hoverSort = sort;
break; break;
} }
} }
for (SongSort sort : SongSort.VALUES_REVERSED) { for (BeatmapSortOrder sort : BeatmapSortOrder.VALUES_REVERSED) {
if (sort != currentSort) if (sort != currentSort)
sort.draw(false, sort == hoverSort); sort.draw(false, sort == hoverSort);
} }
@@ -431,14 +437,14 @@ public class SongMenu extends BasicGameState {
// scroll bar // scroll bar
if (focusNode != null) { if (focusNode != null) {
int focusNodes = focusNode.osuFiles.size(); int focusNodes = focusNode.getBeatmapSet().size();
int totalNodes = OsuGroupList.get().size() + focusNodes - 1; int totalNodes = BeatmapSetList.get().size() + focusNodes - 1;
if (totalNodes > MAX_SONG_BUTTONS) { if (totalNodes > MAX_SONG_BUTTONS) {
int startIndex = startNode.index; int startIndex = startNode.index;
if (startNode.index > focusNode.index) if (startNode.index > focusNode.index)
startIndex += focusNodes; startIndex += focusNodes;
else if (startNode.index == focusNode.index) else if (startNode.index == focusNode.index)
startIndex += startNode.osuFileIndex; startIndex += startNode.beatmapIndex;
UI.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS, UI.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS,
width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset, width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset,
Utils.COLOR_BLACK_ALPHA, Color.white, true); Utils.COLOR_BLACK_ALPHA, Color.white, true);
@@ -495,9 +501,9 @@ public class SongMenu extends BasicGameState {
// store the start/focus nodes // store the start/focus nodes
if (focusNode != null) 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 // reset song stack
randomStack = new Stack<SongNode>(); randomStack = new Stack<SongNode>();
@@ -509,19 +515,19 @@ public class SongMenu extends BasicGameState {
startNode = focusNode = null; startNode = focusNode = null;
scoreMap = null; scoreMap = null;
focusScores = null; focusScores = null;
if (OsuGroupList.get().size() > 0) { if (BeatmapSetList.get().size() > 0) {
OsuGroupList.get().init(); BeatmapSetList.get().init();
if (search.getText().isEmpty()) { // cleared search if (search.getText().isEmpty()) { // cleared search
// use previous start/focus if possible // use previous start/focus if possible
if (oldFocusNode != null) if (oldFocusNode != null)
setFocus(oldFocusNode.getNode(), oldFocusNode.getIndex(), true, true); setFocus(oldFocusNode.getNode(), oldFocusNode.getIndex(), true, true);
else else
setFocus(OsuGroupList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else { } else {
int size = OsuGroupList.get().size(); int size = BeatmapSetList.get().size();
searchResultString = String.format("%d match%s found!", searchResultString = String.format("%d match%s found!",
size, (size == 1) ? "" : "es"); size, (size == 1) ? "" : "es");
setFocus(OsuGroupList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} }
oldFocusNode = null; oldFocusNode = null;
} else if (!search.getText().isEmpty()) } else if (!search.getText().isEmpty())
@@ -549,9 +555,9 @@ public class SongMenu extends BasicGameState {
// mouse hover // mouse hover
boolean isHover = false; boolean isHover = false;
if (mouseY > headerY && mouseY < footerY) { 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) { 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) && if ((mouseX > cx && mouseX < cx + buttonWidth) &&
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) { (mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
if (i == hoverIndex) { if (i == hoverIndex) {
@@ -630,15 +636,15 @@ public class SongMenu extends BasicGameState {
return; return;
// sorting buttons // sorting buttons
for (SongSort sort : SongSort.values()) { for (BeatmapSortOrder sort : BeatmapSortOrder.values()) {
if (sort.contains(x, y)) { if (sort.contains(x, y)) {
if (sort != SongSort.getSort()) { if (sort != BeatmapSortOrder.getSort()) {
SongSort.setSort(sort); BeatmapSortOrder.setSort(sort);
SoundController.playSound(SoundEffect.MENUCLICK); SoundController.playSound(SoundEffect.MENUCLICK);
OsuGroupNode oldFocusBase = OsuGroupList.get().getBaseNode(focusNode.index); BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
int oldFocusFileIndex = focusNode.osuFileIndex; int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null; focusNode = null;
OsuGroupList.get().init(); BeatmapSetList.get().init();
setFocus(oldFocusBase, oldFocusFileIndex, true, true); setFocus(oldFocusBase, oldFocusFileIndex, true, true);
} }
return; return;
@@ -647,8 +653,8 @@ public class SongMenu extends BasicGameState {
// song buttons // song buttons
if (y > headerY && y < footerY) { if (y > headerY && y < footerY) {
int expandedIndex = OsuGroupList.get().getExpandedIndex(); int expandedIndex = BeatmapSetList.get().getExpandedIndex();
OsuGroupNode node = startNode; BeatmapSetNode node = startNode;
for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) {
// is button at this index clicked? // is button at this index clicked?
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX; float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
@@ -659,7 +665,7 @@ public class SongMenu extends BasicGameState {
// clicked node is already expanded // clicked node is already expanded
if (node.index == expandedIndex) { if (node.index == expandedIndex) {
if (node.osuFileIndex == focusNode.osuFileIndex) { if (node.beatmapIndex == focusNode.beatmapIndex) {
// if already focused, load the beatmap // if already focused, load the beatmap
if (button != Input.MOUSE_RIGHT_BUTTON) if (button != Input.MOUSE_RIGHT_BUTTON)
startGame(); startGame();
@@ -724,7 +730,7 @@ public class SongMenu extends BasicGameState {
switch (key) { switch (key) {
case Input.KEY_ESCAPE: case Input.KEY_ESCAPE:
if (reloadThread != null) { 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(); reloadThread.interrupt();
} else if (!search.getText().isEmpty()) { } else if (!search.getText().isEmpty()) {
// clear search text // clear search text
@@ -755,8 +761,8 @@ public class SongMenu extends BasicGameState {
setFocus(prev.getNode(), prev.getIndex(), true, true); setFocus(prev.getNode(), prev.getIndex(), true, true);
} else { } else {
// random track, add previous to stack // random track, add previous to stack
randomStack.push(new SongNode(OsuGroupList.get().getBaseNode(focusNode.index), focusNode.osuFileIndex)); randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex));
setFocus(OsuGroupList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} }
break; break;
case Input.KEY_F3: case Input.KEY_F3:
@@ -776,7 +782,7 @@ public class SongMenu extends BasicGameState {
break; break;
if (input.isKeyDown(Input.KEY_RSHIFT) || input.isKeyDown(Input.KEY_LSHIFT)) { if (input.isKeyDown(Input.KEY_RSHIFT) || input.isKeyDown(Input.KEY_LSHIFT)) {
SoundController.playSound(SoundEffect.MENUHIT); 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; MenuState.BEATMAP_DELETE_CONFIRM : MenuState.BEATMAP_DELETE_SELECT;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, focusNode); ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(ms, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU); game.enterState(Opsu.STATE_BUTTONMENU);
@@ -810,10 +816,10 @@ public class SongMenu extends BasicGameState {
case Input.KEY_RIGHT: case Input.KEY_RIGHT:
if (focusNode == null) if (focusNode == null)
break; break;
OsuGroupNode next = focusNode.next; BeatmapSetNode next = focusNode.next;
if (next != null) { if (next != null) {
SoundController.playSound(SoundEffect.MENUCLICK); SoundController.playSound(SoundEffect.MENUCLICK);
OsuGroupNode oldStartNode = startNode; BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset; float oldHoverOffset = hoverOffset;
int oldHoverIndex = hoverIndex; int oldHoverIndex = hoverIndex;
setFocus(next, 0, false, true); setFocus(next, 0, false, true);
@@ -826,13 +832,13 @@ public class SongMenu extends BasicGameState {
case Input.KEY_LEFT: case Input.KEY_LEFT:
if (focusNode == null) if (focusNode == null)
break; break;
OsuGroupNode prev = focusNode.prev; BeatmapSetNode prev = focusNode.prev;
if (prev != null) { if (prev != null) {
SoundController.playSound(SoundEffect.MENUCLICK); SoundController.playSound(SoundEffect.MENUCLICK);
OsuGroupNode oldStartNode = startNode; BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset; float oldHoverOffset = hoverOffset;
int oldHoverIndex = hoverIndex; 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) { if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset; hoverOffset = oldHoverOffset;
hoverIndex = oldHoverIndex; hoverIndex = oldHoverIndex;
@@ -937,18 +943,19 @@ public class SongMenu extends BasicGameState {
startScore = 0; startScore = 0;
beatmapMenuTimer = -1; beatmapMenuTimer = -1;
searchTransitionTimer = SEARCH_TRANSITION_TIME; searchTransitionTimer = SEARCH_TRANSITION_TIME;
songInfo = null;
// reset song stack // reset song stack
randomStack = new Stack<SongNode>(); randomStack = new Stack<SongNode>();
// set focus node if not set (e.g. theme song playing) // set focus node if not set (e.g. theme song playing)
if (focusNode == null && OsuGroupList.get().size() > 0) if (focusNode == null && BeatmapSetList.get().size() > 0)
setFocus(OsuGroupList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
// reset music track // reset music track
else if (resetTrack) { else if (resetTrack) {
MusicController.pause(); MusicController.pause();
MusicController.playAt(MusicController.getOsuFile().previewTime, true); MusicController.playAt(MusicController.getBeatmap().previewTime, true);
resetTrack = false; resetTrack = false;
} }
@@ -975,7 +982,7 @@ public class SongMenu extends BasicGameState {
// reload scores // reload scores
if (focusNode != null) { if (focusNode != null) {
scoreMap = ScoreDB.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex)); scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
focusScores = getScoreDataForNode(focusNode, true); focusScores = getScoreDataForNode(focusNode, true);
} }
@@ -986,31 +993,31 @@ public class SongMenu extends BasicGameState {
if (stateAction != null) { if (stateAction != null) {
switch (stateAction) { switch (stateAction) {
case BEATMAP: // clear all scores case BEATMAP: // clear all scores
if (stateActionNode == null || stateActionNode.osuFileIndex == -1) if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
break; break;
OsuFile osu = stateActionNode.osuFiles.get(stateActionNode.osuFileIndex); Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
ScoreDB.deleteScore(osu); ScoreDB.deleteScore(beatmap);
if (stateActionNode == focusNode) { if (stateActionNode == focusNode) {
focusScores = null; focusScores = null;
scoreMap.remove(osu.version); scoreMap.remove(beatmap.version);
} }
break; break;
case SCORE: // clear single score case SCORE: // clear single score
if (stateActionScore == null) if (stateActionScore == null)
break; break;
ScoreDB.deleteScore(stateActionScore); ScoreDB.deleteScore(stateActionScore);
scoreMap = ScoreDB.getMapSetScores(focusNode.osuFiles.get(focusNode.osuFileIndex)); scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
focusScores = getScoreDataForNode(focusNode, true); focusScores = getScoreDataForNode(focusNode, true);
startScore = 0; startScore = 0;
break; break;
case BEATMAP_DELETE_CONFIRM: // delete song group case BEATMAP_DELETE_CONFIRM: // delete song group
if (stateActionNode == null) if (stateActionNode == null)
break; break;
OsuGroupNode BeatmapSetNode
prev = OsuGroupList.get().getBaseNode(stateActionNode.index - 1), prev = BeatmapSetList.get().getBaseNode(stateActionNode.index - 1),
next = OsuGroupList.get().getBaseNode(stateActionNode.index + 1); next = BeatmapSetList.get().getBaseNode(stateActionNode.index + 1);
int oldIndex = stateActionNode.index, focusNodeIndex = focusNode.index, startNodeIndex = startNode.index; int oldIndex = stateActionNode.index, focusNodeIndex = focusNode.index, startNodeIndex = startNode.index;
OsuGroupList.get().deleteSongGroup(stateActionNode); BeatmapSetList.get().deleteSongGroup(stateActionNode);
if (oldIndex == focusNodeIndex) { if (oldIndex == focusNodeIndex) {
if (prev != null) if (prev != null)
setFocus(prev, -1, true, true); setFocus(prev, -1, true, true);
@@ -1039,7 +1046,7 @@ public class SongMenu extends BasicGameState {
if (stateActionNode == null) if (stateActionNode == null)
break; break;
int index = stateActionNode.index; int index = stateActionNode.index;
OsuGroupList.get().deleteSong(stateActionNode); BeatmapSetList.get().deleteSong(stateActionNode);
if (stateActionNode == focusNode) { if (stateActionNode == focusNode) {
if (stateActionNode.prev != null && if (stateActionNode.prev != null &&
!(stateActionNode.next != null && stateActionNode.next.index == index)) { !(stateActionNode.next != null && stateActionNode.next.index == index)) {
@@ -1081,17 +1088,17 @@ public class SongMenu extends BasicGameState {
@Override @Override
public void run() { public void run() {
// clear the beatmap cache // clear the beatmap cache
OsuDB.clearDatabase(); BeatmapDB.clearDatabase();
// invoke unpacker and parser // invoke unpacker and parser
File beatmapDir = Options.getBeatmapDir(); File beatmapDir = Options.getBeatmapDir();
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir); OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
OsuParser.parseAllFiles(beatmapDir); BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list // initialize song list
if (OsuGroupList.get().size() > 0) { if (BeatmapSetList.get().size() > 0) {
OsuGroupList.get().init(); BeatmapSetList.get().init();
setFocus(OsuGroupList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else } else
MusicController.playThemeSong(); MusicController.playThemeSong();
@@ -1139,7 +1146,7 @@ public class SongMenu extends BasicGameState {
n++; n++;
shifted = true; shifted = true;
} else if (n > 0 && startNode.next != null && } 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; startNode = startNode.next;
buttonY -= buttonOffset / 4; buttonY -= buttonOffset / 4;
if (buttonY < headerY - height * 0.02f) if (buttonY < headerY - height * 0.02f)
@@ -1159,69 +1166,69 @@ public class SongMenu extends BasicGameState {
/** /**
* Sets a new focus node. * Sets a new focus node.
* @param node the base node; it will be expanded if it isn't already * @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 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) * @param preview whether to start at the preview time (true) or beginning (false)
* @return the old focus node * @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) if (node == null)
return null; return null;
hoverOffset = 0f; hoverOffset = 0f;
hoverIndex = -1; hoverIndex = -1;
songInfo = null; songInfo = null;
OsuGroupNode oldFocus = focusNode; BeatmapSetNode oldFocus = focusNode;
// expand node before focusing it // expand node before focusing it
int expandedIndex = OsuGroupList.get().getExpandedIndex(); int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index != expandedIndex) { 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 start node was previously expanded, move it
if (startNode != null && startNode.index == expandedIndex) if (startNode != null && startNode.index == expandedIndex)
startNode = OsuGroupList.get().getBaseNode(startNode.index); startNode = BeatmapSetList.get().getBaseNode(startNode.index);
} }
// check osuFileIndex bounds // check beatmapIndex bounds
int length = node.osuFiles.size(); int length = node.getBeatmapSet().size();
if (osuFileIndex < 0 || osuFileIndex > length - 1) // set a random index if (beatmapIndex < 0 || beatmapIndex > length - 1) // set a random index
osuFileIndex = (int) (Math.random() * length); beatmapIndex = (int) (Math.random() * length);
// change the focus node // 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; startNode = node;
focusNode = OsuGroupList.get().getNode(node, osuFileIndex); focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
OsuFile osu = focusNode.osuFiles.get(focusNode.osuFileIndex); Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
MusicController.play(osu, false, preview); MusicController.play(beatmap, false, preview);
// load scores // load scores
scoreMap = ScoreDB.getMapSetScores(osu); scoreMap = ScoreDB.getMapSetScores(beatmap);
focusScores = getScoreDataForNode(focusNode, true); focusScores = getScoreDataForNode(focusNode, true);
startScore = 0; startScore = 0;
// check startNode bounds // 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; startNode = startNode.prev;
// make sure focusNode is on the screen (TODO: cleanup...) // 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 if (val > 0) // below screen
changeIndex(val); changeIndex(val);
else { // above screen else { // above screen
if (focusNode.index == startNode.index) { 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) if (val < 0)
changeIndex(val); changeIndex(val);
} else if (startNode.index > focusNode.index) { } 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) if (val < 0)
changeIndex(val); changeIndex(val);
} }
} }
// if start node is expanded and on group node, move it // 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); changeIndex(1);
return oldFocus; return oldFocus;
@@ -1248,7 +1255,7 @@ public class SongMenu extends BasicGameState {
* @param menuState the menu state determining the action * @param menuState the menu state determining the action
* @param node the song node to perform the action on * @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); doStateActionOnLoad(menuState, node, null);
} }
@@ -1267,32 +1274,32 @@ public class SongMenu extends BasicGameState {
* @param node the song node to perform the action on * @param node the song node to perform the action on
* @param scoreData the score data 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; stateAction = menuState;
stateActionNode = node; stateActionNode = node;
stateActionScore = scoreData; 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. * 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 * @param setTimeSince whether or not to set the "time since" field for the scores
* @return the ScoreData array * @return the ScoreData array
*/ */
private ScoreData[] getScoreDataForNode(OsuGroupNode node, boolean setTimeSince) { private ScoreData[] getScoreDataForNode(BeatmapSetNode node, boolean setTimeSince) {
if (scoreMap == null || scoreMap.isEmpty() || node.osuFileIndex == -1) // node not expanded if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
return null; return null;
OsuFile osu = node.osuFiles.get(node.osuFileIndex); Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
ScoreData[] scores = scoreMap.get(osu.version); ScoreData[] scores = scoreMap.get(beatmap.version);
if (scores == null || scores.length < 1) // no scores if (scores == null || scores.length < 1) // no scores
return null; return null;
ScoreData s = scores[0]; ScoreData s = scores[0];
if (osu.beatmapID == s.MID && osu.beatmapSetID == s.MSID && if (beatmap.beatmapID == s.MID && beatmap.beatmapSetID == s.MSID &&
osu.title.equals(s.title) && osu.artist.equals(s.artist) && beatmap.title.equals(s.title) && beatmap.artist.equals(s.artist) &&
osu.creator.equals(s.creator)) { beatmap.creator.equals(s.creator)) {
if (setTimeSince) { if (setTimeSince) {
for (int i = 0; i < scores.length; i++) for (int i = 0; i < scores.length; i++)
scores[i].getTimeSince(); scores[i].getTimeSince();
@@ -1311,9 +1318,9 @@ public class SongMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
MultiClip.destroyExtraClips(); MultiClip.destroyExtraClips();
OsuFile osu = MusicController.getOsuFile(); Beatmap beatmap = MusicController.getBeatmap();
Game gameState = (Game) game.getState(Opsu.STATE_GAME); Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadOsuFile(osu); gameState.loadBeatmap(beatmap);
gameState.setRestart(Game.Restart.NEW); gameState.setRestart(Game.Restart.NEW);
gameState.setReplay(null); gameState.setReplay(null);
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));

View File

@@ -21,14 +21,15 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OsuGroupList;
import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.UI;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.replay.ReplayImporter;
//conflict
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
@@ -56,6 +57,9 @@ public class Splash extends BasicGameState {
/** Number of times the 'Esc' key has been pressed. */ /** Number of times the 'Esc' key has been pressed. */
private int escapeCount = 0; private int escapeCount = 0;
/** Whether the skin being loaded is a new skin (for program restarts). */
private boolean newSkin = false;
// game-related variables // game-related variables
private int state; private int state;
private GameContainer container; private GameContainer container;
@@ -70,6 +74,10 @@ public class Splash extends BasicGameState {
throws SlickException { throws SlickException {
this.container = container; 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) // load Utils class first (needed in other 'init' methods)
Utils.init(container, game); Utils.init(container, game);
@@ -90,11 +98,27 @@ public class Splash extends BasicGameState {
if (!init) { if (!init) {
init = true; init = true;
if (OsuGroupList.get() != null) { // resources already loaded (from application restart)
// resources already loaded (from application restart) if (BeatmapSetList.get() != null) {
finished = true; // reload sounds if skin changed
} else { if (newSkin) {
// load resources in a new thread 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() { thread = new Thread() {
@Override @Override
public void run() { public void run() {
@@ -104,7 +128,7 @@ public class Splash extends BasicGameState {
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir); OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
// parse song directory // parse song directory
OsuParser.parseAllFiles(beatmapDir); BeatmapParser.parseAllFiles(beatmapDir);
// import replays // import replays
ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir()); ReplayImporter.importAllReplaysFromDir(Options.getReplayImportDir());
@@ -129,12 +153,12 @@ public class Splash extends BasicGameState {
// change states when loading complete // change states when loading complete
if (finished && alpha >= 1f) { if (finished && alpha >= 1f) {
// initialize song list // initialize song list
if (OsuGroupList.get().size() > 0) { if (BeatmapSetList.get().size() > 0) {
OsuGroupList.get().init(); BeatmapSetList.get().init();
if (Options.isThemeSongEnabled()) if (Options.isThemeSongEnabled())
MusicController.playThemeSong(); MusicController.playThemeSong();
else 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 // play the theme song
@@ -155,7 +179,7 @@ public class Splash extends BasicGameState {
if (++escapeCount >= 3) if (++escapeCount >= 3)
container.exit(); container.exit();
// stop parsing OsuFiles by sending interrupt to OsuParser // stop parsing beatmaps by sending interrupt to BeatmapParser
else if (thread != null) else if (thread != null)
thread.interrupt(); thread.interrupt();
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer> 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<Integer>();
cursorY = new LinkedList<Integer>();
}
/**
* 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<Integer> iterX = cursorX.iterator();
Iterator<Integer> 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();
}
}

View File

@@ -16,7 +16,9 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>. * along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/ */
package itdelatrisu.opsu; package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.Utils;
import org.newdawn.slick.Animation; import org.newdawn.slick.Animation;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
@@ -146,7 +148,7 @@ public class MenuButton {
* Sets text to draw in the middle of the button. * Sets text to draw in the middle of the button.
* @param text the text to draw * @param text the text to draw
* @param font the font to use when drawing * @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) { public void setText(String text, Font font, Color color) {
this.text = text; this.text = text;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -555,8 +555,8 @@ public abstract class GameContainer implements GUIContext {
* bottom. * bottom.
* *
* @param ref The reference to the image to be loaded * @param ref The reference to the image to be loaded
* @param x The x-coordinate of the cursor hotspot (left -> right) * @param x The x-coordinate of the cursor hotspot (left {@literal ->} right)
* @param y The y-coordinate of the cursor hotspot (bottom -> top) * @param y The y-coordinate of the cursor hotspot (bottom {@literal ->} top)
* @param width The x width of the cursor * @param width The x width of the cursor
* @param height The y height of the cursor * @param height The y height of the cursor
* @param cursorDelays image delays between changing frames in animation * @param cursorDelays image delays between changing frames in animation

View File

@@ -935,7 +935,7 @@ public class Image implements Renderable {
/** /**
* Set the angle to rotate this image to. The angle will be normalized to * 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 * @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 * 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. * @param angle The angle to add.
*/ */

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,7 @@ import org.newdawn.slick.util.Log;
* play at any given time and a channel is reserved so music will always play. * play at any given time and a channel is reserved so music will always play.
* *
* @author kevin * @author kevin
* @author Nathan Sweet <misc@n4te.com> * @author Nathan Sweet {@literal <misc@n4te.com>}
*/ */
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
public class Music { public class Music {

View File

@@ -46,7 +46,7 @@ import org.newdawn.slick.util.ResourceLoader;
* as required. * as required.
* *
* @author Kevin Glass * @author Kevin Glass
* @author Nathan Sweet <misc@n4te.com> * @author Nathan Sweet {@literal <misc@n4te.com>}
* @author Rockstar play and setPosition cleanup * @author Rockstar play and setPosition cleanup
*/ */
public class OpenALStreamPlayer { public class OpenALStreamPlayer {

Binary file not shown.