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
5
.gitignore
vendored
@@ -15,5 +15,10 @@
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
/target
|
/target
|
||||||
|
|||||||
4
pom.xml
@@ -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
BIN
res/lighting.png
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB |
BIN
res/options-background.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
res/playback-double.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
res/playback-half.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
res/playback-normal.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
res/slidergradient.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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 (Options.getSkin().isComboBurstRandom())
|
||||||
|
comboBurstIndex = (int) (Math.random() * comboBurstImages.length);
|
||||||
|
else {
|
||||||
if (combo == 30)
|
if (combo == 30)
|
||||||
comboBurstIndex = 0;
|
comboBurstIndex = 0;
|
||||||
else
|
else
|
||||||
comboBurstIndex = (comboBurstIndex + 1) % comboBurstImages.length;
|
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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 +389,12 @@ 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[] { "" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
try {
|
||||||
DBController.init();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
package itdelatrisu.opsu;
|
package itdelatrisu.opsu;
|
||||||
|
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
|
import itdelatrisu.opsu.skins.Skin;
|
||||||
|
import itdelatrisu.opsu.skins.SkinLoader;
|
||||||
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
@@ -30,6 +34,7 @@ import java.io.OutputStreamWriter;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -37,7 +42,10 @@ import org.lwjgl.input.Keyboard;
|
|||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
import org.newdawn.slick.Input;
|
import org.newdawn.slick.Input;
|
||||||
import org.newdawn.slick.SlickException;
|
import org.newdawn.slick.SlickException;
|
||||||
|
import org.newdawn.slick.util.ClasspathLocation;
|
||||||
|
import org.newdawn.slick.util.FileSystemLocation;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
|
import org.newdawn.slick.util.ResourceLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all user options.
|
* Handles all user options.
|
||||||
@@ -62,14 +70,21 @@ public class Options {
|
|||||||
new File(DATA_DIR, "Songs/").getPath()
|
new File(DATA_DIR, "Songs/").getPath()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Skin directories (where to search for skins). */
|
||||||
|
private static final String[] SKIN_ROOT_DIRS = {
|
||||||
|
"C:/Program Files (x86)/osu!/Skins/",
|
||||||
|
"C:/Program Files/osu!/Skins/",
|
||||||
|
new File(DATA_DIR, "Skins/").getPath()
|
||||||
|
};
|
||||||
|
|
||||||
/** Cached beatmap database name. */
|
/** Cached beatmap database name. */
|
||||||
public static final File OSU_DB = new File(DATA_DIR, ".opsu.db");
|
public static final File BEATMAP_DB = new File(DATA_DIR, ".opsu.db");
|
||||||
|
|
||||||
/** Score database name. */
|
/** Score database name. */
|
||||||
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
|
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
|
||||||
|
|
||||||
/** Font file name. */
|
/** Font file name. */
|
||||||
public static final String FONT_NAME = "kochi-gothic.ttf";
|
public static final String FONT_NAME = "DroidSansFallback.ttf";
|
||||||
|
|
||||||
/** Version file name. */
|
/** Version file name. */
|
||||||
public static final String VERSION_FILE = "version";
|
public static final String VERSION_FILE = "version";
|
||||||
@@ -98,8 +113,8 @@ public class Options {
|
|||||||
/** The replay import directory. */
|
/** The replay import directory. */
|
||||||
private static File replayImportDir;
|
private static File replayImportDir;
|
||||||
|
|
||||||
/** The current skin directory (for user skins). */
|
/** The root skin directory. */
|
||||||
private static File skinDir;
|
private static File skinRootDir;
|
||||||
|
|
||||||
/** Port binding. */
|
/** Port binding. */
|
||||||
private static int port = 49250;
|
private static int port = 49250;
|
||||||
@@ -140,8 +155,63 @@ public class Options {
|
|||||||
|
|
||||||
/** Game options. */
|
/** Game options. */
|
||||||
public enum GameOption {
|
public enum GameOption {
|
||||||
NULL (null, null),
|
// internal options (not displayed in-game)
|
||||||
SCREEN_RESOLUTION ("Screen Resolution", "Restart (Ctrl+Shift+F5) to apply resolution changes.") {
|
BEATMAP_DIRECTORY ("BeatmapDirectory") {
|
||||||
|
@Override
|
||||||
|
public String write() { return getBeatmapDir().getAbsolutePath(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { beatmapDir = new File(s); }
|
||||||
|
},
|
||||||
|
OSZ_DIRECTORY ("OSZDirectory") {
|
||||||
|
@Override
|
||||||
|
public String write() { return getOSZDir().getAbsolutePath(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { oszDir = new File(s); }
|
||||||
|
},
|
||||||
|
SCREENSHOT_DIRECTORY ("ScreenshotDirectory") {
|
||||||
|
@Override
|
||||||
|
public String write() { return getScreenshotDir().getAbsolutePath(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { screenshotDir = new File(s); }
|
||||||
|
},
|
||||||
|
REPLAY_DIRECTORY ("ReplayDirectory") {
|
||||||
|
@Override
|
||||||
|
public String write() { return getReplayDir().getAbsolutePath(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { replayDir = new File(s); }
|
||||||
|
},
|
||||||
|
SKIN_DIRECTORY ("SkinDirectory") {
|
||||||
|
@Override
|
||||||
|
public String write() { return getSkinRootDir().getAbsolutePath(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { skinRootDir = new File(s); }
|
||||||
|
},
|
||||||
|
THEME_SONG ("ThemeSong") {
|
||||||
|
@Override
|
||||||
|
public String write() { return themeString; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { themeString = s; }
|
||||||
|
},
|
||||||
|
PORT ("Port") {
|
||||||
|
@Override
|
||||||
|
public String write() { return Integer.toString(port); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = Integer.parseInt(s);
|
||||||
|
if (i > 0 && i < 65535)
|
||||||
|
port = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// in-game options
|
||||||
|
SCREEN_RESOLUTION ("Screen Resolution", "ScreenResolution", "Restart (Ctrl+Shift+F5) to apply resolution changes.") {
|
||||||
@Override
|
@Override
|
||||||
public String getValueString() { return resolution.toString(); }
|
public String getValueString() { return resolution.toString(); }
|
||||||
|
|
||||||
@@ -149,13 +219,34 @@ public class Options {
|
|||||||
public void click(GameContainer container) {
|
public void click(GameContainer container) {
|
||||||
do {
|
do {
|
||||||
resolution = resolution.next();
|
resolution = resolution.next();
|
||||||
} while (resolution != Resolution.RES_800_600 &&
|
} while (resolution != Resolution.RES_800_600 && (
|
||||||
(container.getScreenWidth() < resolution.getWidth() ||
|
container.getScreenWidth() < resolution.getWidth() ||
|
||||||
container.getScreenHeight() < resolution.getHeight()));
|
container.getScreenHeight() < resolution.getHeight()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
try {
|
||||||
|
Resolution res = Resolution.valueOf(String.format("RES_%s", s.replace('x', '_')));
|
||||||
|
resolution = res;
|
||||||
|
} catch (IllegalArgumentException e) {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// FULLSCREEN ("Fullscreen Mode", "Restart to apply changes.", false),
|
// FULLSCREEN ("Fullscreen Mode", "Fullscreen", "Restart to apply changes.", false),
|
||||||
TARGET_FPS ("Frame Limiter", "Higher values may cause high CPU usage.") {
|
SKIN ("Skin", "Skin", "Restart (Ctrl+Shift+F5) to apply skin changes.") {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return skinName; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void click(GameContainer container) {
|
||||||
|
skinDirIndex = (skinDirIndex + 1) % skinDirs.length;
|
||||||
|
skinName = skinDirs[skinDirIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { skinName = s; }
|
||||||
|
},
|
||||||
|
TARGET_FPS ("Frame Limiter", "FrameSync", "Higher values may cause high CPU usage.") {
|
||||||
@Override
|
@Override
|
||||||
public String getValueString() {
|
public String getValueString() {
|
||||||
return String.format((getTargetFPS() == 60) ? "%dfps (vsync)" : "%dfps", getTargetFPS());
|
return String.format((getTargetFPS() == 60) ? "%dfps (vsync)" : "%dfps", getTargetFPS());
|
||||||
@@ -167,86 +258,23 @@ public class Options {
|
|||||||
container.setTargetFrameRate(getTargetFPS());
|
container.setTargetFrameRate(getTargetFPS());
|
||||||
container.setVSync(getTargetFPS() == 60);
|
container.setVSync(getTargetFPS() == 60);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
MASTER_VOLUME ("Master Volume", "Global volume level.", 35, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public void drag(GameContainer container, int d) {
|
|
||||||
super.drag(container, d);
|
|
||||||
container.setMusicVolume(getMasterVolume() * getMusicVolume());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MUSIC_VOLUME ("Music Volume", "Volume of music.", 80, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public void drag(GameContainer container, int d) {
|
|
||||||
super.drag(container, d);
|
|
||||||
container.setMusicVolume(getMasterVolume() * getMusicVolume());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EFFECT_VOLUME ("Effect Volume", "Volume of menu and game sounds.", 70, 0, 100),
|
|
||||||
HITSOUND_VOLUME ("Hit Sound Volume", "Volume of hit sounds.", 30, 0, 100),
|
|
||||||
MUSIC_OFFSET ("Music Offset", "Adjust this value if hit objects are out of sync.", -75, -500, 500) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return String.format("%dms", val); }
|
|
||||||
},
|
|
||||||
SCREENSHOT_FORMAT ("Screenshot Format", "Press F12 to take a screenshot.") {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return screenshotFormat[screenshotFormatIndex].toUpperCase(); }
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void click(GameContainer container) { screenshotFormatIndex = (screenshotFormatIndex + 1) % screenshotFormat.length; }
|
public String write() { return Integer.toString(targetFPS[targetFPSindex]); }
|
||||||
},
|
|
||||||
SHOW_FPS ("Show FPS Counter", "Show an FPS counter in the bottom-right hand corner.", true),
|
|
||||||
SHOW_HIT_LIGHTING ("Show Hit Lighting", "Adds an effect behind hit explosions.", true),
|
|
||||||
SHOW_COMBO_BURSTS ("Show Combo Bursts", "A character image is displayed at combo milestones.", true),
|
|
||||||
SHOW_PERFECT_HIT ("Show Perfect Hits", "Whether to show perfect hit result bursts (300s, slider ticks).", true),
|
|
||||||
SHOW_FOLLOW_POINTS ("Show Follow Points", "Whether to show follow points between hit objects.", true),
|
|
||||||
NEW_CURSOR ("Enable New Cursor", "Use the new cursor style (may cause higher CPU usage).", true) {
|
|
||||||
@Override
|
@Override
|
||||||
public void click(GameContainer container) {
|
public void read(String s) {
|
||||||
super.click(container);
|
int i = Integer.parseInt(s);
|
||||||
UI.resetCursor();
|
for (int j = 0; j < targetFPS.length; j++) {
|
||||||
|
if (i == targetFPS[j]) {
|
||||||
|
targetFPSindex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "The song background will be used as the main menu background.", true),
|
SHOW_FPS ("Show FPS Counter", "FpsCounter", "Show an FPS counter in the bottom-right hand corner.", true),
|
||||||
BACKGROUND_DIM ("Background Dim", "Percentage to dim the background image during gameplay.", 50, 0, 100),
|
SHOW_UNICODE ("Prefer Non-English Metadata", "ShowUnicode", "Where available, song titles will be shown in their native language.", false) {
|
||||||
FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "Override the song background with the default playfield background.", false),
|
|
||||||
IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "Never use skin element overrides provided by beatmaps.", false),
|
|
||||||
FIXED_CS ("Fixed Circle Size (CS)", "Determines the size of circles and sliders.", 0, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
|
||||||
},
|
|
||||||
FIXED_HP ("Fixed HP Drain Rate (HP)", "Determines the rate at which health decreases.", 0, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
|
||||||
},
|
|
||||||
FIXED_AR ("Fixed Approach Rate (AR)", "Determines how long hit circles stay on the screen.", 0, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
|
||||||
},
|
|
||||||
FIXED_OD ("Fixed Overall Difficulty (OD)", "Determines the time window for hit results.", 0, 0, 100) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
|
||||||
},
|
|
||||||
LOAD_VERBOSE ("Show Detailed Loading Progress", "Display more specific loading information in the splash screen.", false),
|
|
||||||
CHECKPOINT ("Track Checkpoint", "Press Ctrl+L while playing to load a checkpoint, and Ctrl+S to set one.", 0, 0, 3599) {
|
|
||||||
@Override
|
|
||||||
public String getValueString() {
|
|
||||||
return (val == 0) ? "Disabled" : String.format("%02d:%02d",
|
|
||||||
TimeUnit.SECONDS.toMinutes(val),
|
|
||||||
val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DISABLE_SOUNDS ("Disable All Sound Effects", "May resolve Linux sound driver issues. Requires a restart.",
|
|
||||||
(System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
|
|
||||||
KEY_LEFT ("Left Game Key", "Select this option to input a key.") {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
|
|
||||||
},
|
|
||||||
KEY_RIGHT ("Right Game Key", "Select this option to input a key.") {
|
|
||||||
@Override
|
|
||||||
public String getValueString() { return Keyboard.getKeyName(getGameKeyRight()); }
|
|
||||||
},
|
|
||||||
SHOW_UNICODE ("Prefer Non-English Metadata", "Where available, song titles will be shown in their native language.", false) {
|
|
||||||
@Override
|
@Override
|
||||||
public void click(GameContainer container) {
|
public void click(GameContainer container) {
|
||||||
super.click(container);
|
super.click(container);
|
||||||
@@ -261,15 +289,157 @@ public class Options {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ENABLE_THEME_SONG ("Enable Theme Song", "Whether to play the theme song upon starting opsu!", true),
|
SCREENSHOT_FORMAT ("Screenshot Format", "ScreenshotFormat", "Press F12 to take a screenshot.") {
|
||||||
SHOW_HIT_ERROR_BAR ("Show Hit Error Bar", "Shows precisely how accurate you were with each hit.", false),
|
@Override
|
||||||
LOAD_HD_IMAGES ("Load HD Images", "Loads HD (@2x) images when available. Increases memory usage and loading times.", true),
|
public String getValueString() { return screenshotFormat[screenshotFormatIndex].toUpperCase(); }
|
||||||
DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
|
|
||||||
DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false);
|
@Override
|
||||||
|
public void click(GameContainer container) { screenshotFormatIndex = (screenshotFormatIndex + 1) % screenshotFormat.length; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return Integer.toString(screenshotFormatIndex); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = Integer.parseInt(s);
|
||||||
|
if (i >= 0 && i < screenshotFormat.length)
|
||||||
|
screenshotFormatIndex = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NEW_CURSOR ("Enable New Cursor", "NewCursor", "Use the new cursor style (may cause higher CPU usage).", true) {
|
||||||
|
@Override
|
||||||
|
public void click(GameContainer container) {
|
||||||
|
super.click(container);
|
||||||
|
UI.getCursor().reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "DynamicBackground", "The song background will be used as the main menu background.", true),
|
||||||
|
LOAD_VERBOSE ("Show Detailed Loading Progress", "LoadVerbose", "Display more specific loading information in the splash screen.", false),
|
||||||
|
MASTER_VOLUME ("Master Volume", "VolumeUniversal", "Global volume level.", 35, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public void drag(GameContainer container, int d) {
|
||||||
|
super.drag(container, d);
|
||||||
|
container.setMusicVolume(getMasterVolume() * getMusicVolume());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MUSIC_VOLUME ("Music Volume", "VolumeMusic", "Volume of music.", 80, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public void drag(GameContainer container, int d) {
|
||||||
|
super.drag(container, d);
|
||||||
|
container.setMusicVolume(getMasterVolume() * getMusicVolume());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EFFECT_VOLUME ("Effect Volume", "VolumeEffect", "Volume of menu and game sounds.", 70, 0, 100),
|
||||||
|
HITSOUND_VOLUME ("Hit Sound Volume", "VolumeHitSound", "Volume of hit sounds.", 30, 0, 100),
|
||||||
|
MUSIC_OFFSET ("Music Offset", "Offset", "Adjust this value if hit objects are out of sync.", -75, -500, 500) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return String.format("%dms", val); }
|
||||||
|
},
|
||||||
|
DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.",
|
||||||
|
(System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
|
||||||
|
KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return Keyboard.getKeyName(getGameKeyLeft()); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { setGameKeyLeft(Keyboard.getKeyIndex(s)); }
|
||||||
|
},
|
||||||
|
KEY_RIGHT ("Right Game Key", "keyOsuRight", "Select this option to input a key.") {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return Keyboard.getKeyName(getGameKeyRight()); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return Keyboard.getKeyName(getGameKeyRight()); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) { setGameKeyRight(Keyboard.getKeyIndex(s)); }
|
||||||
|
},
|
||||||
|
DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "MouseDisableWheel", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
|
||||||
|
DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "MouseDisableButtons", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false),
|
||||||
|
BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100),
|
||||||
|
FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false),
|
||||||
|
IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false),
|
||||||
|
SHOW_HIT_LIGHTING ("Show Hit Lighting", "HitLighting", "Adds an effect behind hit explosions.", true),
|
||||||
|
SHOW_COMBO_BURSTS ("Show Combo Bursts", "ComboBurst", "A character image is displayed at combo milestones.", true),
|
||||||
|
SHOW_PERFECT_HIT ("Show Perfect Hits", "PerfectHit", "Whether to show perfect hit result bursts (300s, slider ticks).", true),
|
||||||
|
SHOW_FOLLOW_POINTS ("Show Follow Points", "FollowPoints", "Whether to show follow points between hit objects.", true),
|
||||||
|
SHOW_HIT_ERROR_BAR ("Show Hit Error Bar", "ScoreMeter", "Shows precisely how accurate you were with each hit.", false),
|
||||||
|
LOAD_HD_IMAGES ("Load HD Images", "LoadHDImages", String.format("Loads HD (%s) images when available. Increases memory usage and loading times.", GameImage.HD_SUFFIX), true),
|
||||||
|
FIXED_CS ("Fixed Circle Size (CS)", "FixedCS", "Determines the size of circles and sliders.", 0, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = (int) (Float.parseFloat(s) * 10f);
|
||||||
|
if (i >= 0 && i <= 100)
|
||||||
|
val = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FIXED_HP ("Fixed HP Drain Rate (HP)", "FixedHP", "Determines the rate at which health decreases.", 0, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = (int) (Float.parseFloat(s) * 10f);
|
||||||
|
if (i >= 0 && i <= 100)
|
||||||
|
val = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FIXED_AR ("Fixed Approach Rate (AR)", "FixedAR", "Determines how long hit circles stay on the screen.", 0, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = (int) (Float.parseFloat(s) * 10f);
|
||||||
|
if (i >= 0 && i <= 100)
|
||||||
|
val = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FIXED_OD ("Fixed Overall Difficulty (OD)", "FixedOD", "Determines the time window for hit results.", 0, 0, 100) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() { return (val == 0) ? "Disabled" : String.format("%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String write() { return String.format(Locale.US, "%.1f", val / 10f); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(String s) {
|
||||||
|
int i = (int) (Float.parseFloat(s) * 10f);
|
||||||
|
if (i >= 0 && i <= 100)
|
||||||
|
val = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CHECKPOINT ("Track Checkpoint", "Checkpoint", "Press Ctrl+L while playing to load a checkpoint, and Ctrl+S to set one.", 0, 0, 3599) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() {
|
||||||
|
return (val == 0) ? "Disabled" : String.format("%02d:%02d",
|
||||||
|
TimeUnit.SECONDS.toMinutes(val),
|
||||||
|
val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true);
|
||||||
|
|
||||||
/** Option name. */
|
/** Option name. */
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/** Option name, as displayed in the configuration file. */
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
/** Option description. */
|
/** Option description. */
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@@ -282,43 +452,58 @@ public class Options {
|
|||||||
/** The upper and lower bounds on the integer value (if applicable). */
|
/** The upper and lower bounds on the integer value (if applicable). */
|
||||||
private int max, min;
|
private int max, min;
|
||||||
|
|
||||||
|
/** Option types. */
|
||||||
|
private enum OptionType { BOOLEAN, NUMERIC, OTHER };
|
||||||
|
|
||||||
/** Whether or not this is a numeric option. */
|
/** Whether or not this is a numeric option. */
|
||||||
private boolean isNumeric;
|
private OptionType type = OptionType.OTHER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor for internal options (not displayed in-game).
|
||||||
|
* @param displayName the option name, as displayed in the configuration file
|
||||||
|
*/
|
||||||
|
GameOption(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for other option types.
|
||||||
* @param name the option name
|
* @param name the option name
|
||||||
|
* @param displayName the option name, as displayed in the configuration file
|
||||||
* @param description the option description
|
* @param description the option description
|
||||||
*/
|
*/
|
||||||
GameOption(String name, String description) {
|
GameOption(String name, String displayName, String description) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.displayName = displayName;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor for boolean options.
|
||||||
* @param name the option name
|
* @param name the option name
|
||||||
|
* @param displayName the option name, as displayed in the configuration file
|
||||||
* @param description the option description
|
* @param description the option description
|
||||||
* @param value the default boolean value
|
* @param value the default boolean value
|
||||||
*/
|
*/
|
||||||
GameOption(String name, String description, boolean value) {
|
GameOption(String name, String displayName, String description, boolean value) {
|
||||||
this(name, description);
|
this(name, displayName, description);
|
||||||
this.bool = value;
|
this.bool = value;
|
||||||
this.isNumeric = false;
|
this.type = OptionType.BOOLEAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor for numeric options.
|
||||||
* @param name the option name
|
* @param name the option name
|
||||||
|
* @param displayName the option name, as displayed in the configuration file
|
||||||
* @param description the option description
|
* @param description the option description
|
||||||
* @param value the default integer value
|
* @param value the default integer value
|
||||||
*/
|
*/
|
||||||
GameOption(String name, String description, int value, int min, int max) {
|
GameOption(String name, String displayName, String description, int value, int min, int max) {
|
||||||
this(name, description);
|
this(name, displayName, description);
|
||||||
this.val = value;
|
this.val = value;
|
||||||
this.min = min;
|
this.min = min;
|
||||||
this.max = max;
|
this.max = max;
|
||||||
this.isNumeric = true;
|
this.type = OptionType.NUMERIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,6 +512,12 @@ public class Options {
|
|||||||
*/
|
*/
|
||||||
public String getName() { return name; }
|
public String getName() { return name; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the option name, as displayed in the configuration file.
|
||||||
|
* @return the display name string
|
||||||
|
*/
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the option description.
|
* Returns the option description.
|
||||||
* @return the description string
|
* @return the description string
|
||||||
@@ -360,15 +551,18 @@ public class Options {
|
|||||||
/**
|
/**
|
||||||
* Returns the value of the option as a string (via override).
|
* Returns the value of the option as a string (via override).
|
||||||
* <p>
|
* <p>
|
||||||
* By default, this returns "{@code val}%" if this is an numeric option,
|
* By default, this returns "{@code val}%" for numeric options,
|
||||||
* or "Yes" or "No" based on the {@code bool} field otherwise.
|
* "Yes" or "No" based on the {@code bool} field for boolean options,
|
||||||
|
* and an empty string otherwise.
|
||||||
* @return the value string
|
* @return the value string
|
||||||
*/
|
*/
|
||||||
public String getValueString() {
|
public String getValueString() {
|
||||||
if (isNumeric)
|
if (type == OptionType.NUMERIC)
|
||||||
return String.format("%d%%", val);
|
return String.format("%d%%", val);
|
||||||
else
|
else if (type == OptionType.BOOLEAN)
|
||||||
return (bool) ? "Yes" : "No";
|
return (bool) ? "Yes" : "No";
|
||||||
|
else
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,17 +576,54 @@ public class Options {
|
|||||||
/**
|
/**
|
||||||
* Processes a mouse drag action (via override).
|
* Processes a mouse drag action (via override).
|
||||||
* <p>
|
* <p>
|
||||||
* By default, if this is a numeric option, the {@code val} field will
|
* By default, only if this is a numeric option, the {@code val} field
|
||||||
* be shifted by {@code d} within the given bounds.
|
* will be shifted by {@code d} within the given bounds.
|
||||||
* @param container the game container
|
* @param container the game container
|
||||||
* @param d the dragged distance (modified by multiplier)
|
* @param d the dragged distance (modified by multiplier)
|
||||||
*/
|
*/
|
||||||
public void drag(GameContainer container, int d) {
|
public void drag(GameContainer container, int d) {
|
||||||
if (isNumeric)
|
if (type == OptionType.NUMERIC)
|
||||||
val = Utils.getBoundedValue(val, d, min, max);
|
val = Utils.getBoundedValue(val, d, min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string to write to the configuration file (via override).
|
||||||
|
* <p>
|
||||||
|
* By default, this returns "{@code val}" for numeric options,
|
||||||
|
* "true" or "false" based on the {@code bool} field for boolean options,
|
||||||
|
* and {@link #getValueString()} otherwise.
|
||||||
|
* @return the string to write
|
||||||
|
*/
|
||||||
|
public String write() {
|
||||||
|
if (type == OptionType.NUMERIC)
|
||||||
|
return Integer.toString(val);
|
||||||
|
else if (type == OptionType.BOOLEAN)
|
||||||
|
return Boolean.toString(bool);
|
||||||
|
else
|
||||||
|
return getValueString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the value of the option from the configuration file (via override).
|
||||||
|
* <p>
|
||||||
|
* By default, this sets {@code val} for numeric options only if the
|
||||||
|
* value is between the min and max bounds, sets {@code bool} for
|
||||||
|
* boolean options, and does nothing otherwise.
|
||||||
|
* @param s the value string read from the configuration file
|
||||||
|
*/
|
||||||
|
public void read(String s) {
|
||||||
|
if (type == OptionType.NUMERIC) {
|
||||||
|
int i = Integer.parseInt(s);
|
||||||
|
if (i >= min && i <= max)
|
||||||
|
val = i;
|
||||||
|
} else if (type == OptionType.BOOLEAN)
|
||||||
|
bool = Boolean.parseBoolean(s);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Map of option display names to GameOptions. */
|
||||||
|
private static HashMap<String, GameOption> optionMap;
|
||||||
|
|
||||||
/** Screen resolutions. */
|
/** Screen resolutions. */
|
||||||
private enum Resolution {
|
private enum Resolution {
|
||||||
RES_800_600 (800, 600),
|
RES_800_600 (800, 600),
|
||||||
@@ -451,6 +682,18 @@ public class Options {
|
|||||||
/** Current screen resolution. */
|
/** Current screen resolution. */
|
||||||
private static Resolution resolution = Resolution.RES_1024_768;
|
private static Resolution resolution = Resolution.RES_1024_768;
|
||||||
|
|
||||||
|
/** The available skin directories. */
|
||||||
|
private static String[] skinDirs;
|
||||||
|
|
||||||
|
/** The index in the skinDirs array. */
|
||||||
|
private static int skinDirIndex = 0;
|
||||||
|
|
||||||
|
/** The name of the skin. */
|
||||||
|
private static String skinName = "Default";
|
||||||
|
|
||||||
|
/** The current skin. */
|
||||||
|
private static Skin skin;
|
||||||
|
|
||||||
/** Frame limiters. */
|
/** Frame limiters. */
|
||||||
private static final int[] targetFPS = { 60, 120, 240, 30, 20, 15, 12 };
|
private static final int[] targetFPS = { 60, 120, 240, 30, 20, 15, 12 };
|
||||||
|
|
||||||
@@ -872,38 +1115,102 @@ public class Options {
|
|||||||
* If invalid, this will create a "Skins" folder in the root directory.
|
* If invalid, this will create a "Skins" folder in the root directory.
|
||||||
* @return the skin directory
|
* @return the skin directory
|
||||||
*/
|
*/
|
||||||
public static File getSkinDir() {
|
public static File getSkinRootDir() {
|
||||||
if (skinDir != null && skinDir.isDirectory())
|
if (skinRootDir != null && skinRootDir.isDirectory())
|
||||||
return skinDir;
|
return skinRootDir;
|
||||||
|
|
||||||
skinDir = new File(DATA_DIR, "Skins/");
|
// search for directory
|
||||||
skinDir.mkdir();
|
for (int i = 0; i < SKIN_ROOT_DIRS.length; i++) {
|
||||||
return skinDir;
|
skinRootDir = new File(SKIN_ROOT_DIRS[i]);
|
||||||
|
if (skinRootDir.isDirectory())
|
||||||
|
return skinRootDir;
|
||||||
|
}
|
||||||
|
skinRootDir.mkdir(); // none found, create new directory
|
||||||
|
return skinRootDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a dummy OsuFile containing the theme song.
|
* Loads the skin given by the current skin directory.
|
||||||
* @return the theme song OsuFile
|
* If the directory is invalid, the default skin will be loaded.
|
||||||
*/
|
*/
|
||||||
public static OsuFile getOsuTheme() {
|
public static void loadSkin() {
|
||||||
|
File skinDir = getSkinDir();
|
||||||
|
if (skinDir == null) // invalid skin name
|
||||||
|
skinName = Skin.DEFAULT_SKIN_NAME;
|
||||||
|
|
||||||
|
// create available skins list
|
||||||
|
File[] dirs = SkinLoader.getSkinDirectories(getSkinRootDir());
|
||||||
|
skinDirs = new String[dirs.length + 1];
|
||||||
|
skinDirs[0] = Skin.DEFAULT_SKIN_NAME;
|
||||||
|
for (int i = 0; i < dirs.length; i++)
|
||||||
|
skinDirs[i + 1] = dirs[i].getName();
|
||||||
|
|
||||||
|
// set skin and modify resource locations
|
||||||
|
ResourceLoader.removeAllResourceLocations();
|
||||||
|
if (skinDir == null)
|
||||||
|
skin = new Skin(null);
|
||||||
|
else {
|
||||||
|
// set skin index
|
||||||
|
for (int i = 1; i < skinDirs.length; i++) {
|
||||||
|
if (skinDirs[i].equals(skinName)) {
|
||||||
|
skinDirIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the skin
|
||||||
|
skin = SkinLoader.loadSkin(skinDir);
|
||||||
|
ResourceLoader.addResourceLocation(new FileSystemLocation(skinDir));
|
||||||
|
}
|
||||||
|
ResourceLoader.addResourceLocation(new ClasspathLocation());
|
||||||
|
ResourceLoader.addResourceLocation(new FileSystemLocation(new File(".")));
|
||||||
|
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current skin.
|
||||||
|
* @return the skin, or null if no skin is loaded (see {@link #loadSkin()})
|
||||||
|
*/
|
||||||
|
public static Skin getSkin() { return skin; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current skin directory.
|
||||||
|
* <p>
|
||||||
|
* NOTE: This directory will differ from that of the currently loaded skin
|
||||||
|
* if {@link #loadSkin()} has not been called after a directory change.
|
||||||
|
* Use {@link Skin#getDirectory()} to get the directory of the currently
|
||||||
|
* loaded skin.
|
||||||
|
* @return the skin directory, or null for the default skin
|
||||||
|
*/
|
||||||
|
public static File getSkinDir() {
|
||||||
|
File root = getSkinRootDir();
|
||||||
|
File dir = new File(root, skinName);
|
||||||
|
return (dir.isDirectory()) ? dir : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a dummy Beatmap containing the theme song.
|
||||||
|
* @return the theme song beatmap
|
||||||
|
*/
|
||||||
|
public static Beatmap getThemeBeatmap() {
|
||||||
String[] tokens = themeString.split(",");
|
String[] tokens = themeString.split(",");
|
||||||
if (tokens.length != 4) {
|
if (tokens.length != 4) {
|
||||||
ErrorHandler.error("Theme song string is malformed.", null, false);
|
ErrorHandler.error("Theme song string is malformed.", null, false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
OsuFile osu = new OsuFile(null);
|
Beatmap beatmap = new Beatmap(null);
|
||||||
osu.audioFilename = new File(tokens[0]);
|
beatmap.audioFilename = new File(tokens[0]);
|
||||||
osu.title = tokens[1];
|
beatmap.title = tokens[1];
|
||||||
osu.artist = tokens[2];
|
beatmap.artist = tokens[2];
|
||||||
try {
|
try {
|
||||||
osu.endTime = Integer.parseInt(tokens[3]);
|
beatmap.endTime = Integer.parseInt(tokens[3]);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
ErrorHandler.error("Theme song length is not a valid integer", e, false);
|
ErrorHandler.error("Theme song length is not a valid integer", e, false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return osu;
|
return beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -916,10 +1223,16 @@ public class Options {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create option map
|
||||||
|
if (optionMap == null) {
|
||||||
|
optionMap = new HashMap<String, GameOption>();
|
||||||
|
for (GameOption option : GameOption.values())
|
||||||
|
optionMap.put(option.getDisplayName(), option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// read file
|
||||||
try (BufferedReader in = new BufferedReader(new FileReader(OPTIONS_FILE))) {
|
try (BufferedReader in = new BufferedReader(new FileReader(OPTIONS_FILE))) {
|
||||||
String line;
|
String line;
|
||||||
String name, value;
|
|
||||||
int i;
|
|
||||||
while ((line = in.readLine()) != null) {
|
while ((line = in.readLine()) != null) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (line.length() < 2 || line.charAt(0) == '#')
|
if (line.length() < 2 || line.charAt(0) == '#')
|
||||||
@@ -927,160 +1240,17 @@ public class Options {
|
|||||||
int index = line.indexOf('=');
|
int index = line.indexOf('=');
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
continue;
|
continue;
|
||||||
name = line.substring(0, index).trim();
|
|
||||||
value = line.substring(index + 1).trim();
|
// read option
|
||||||
|
String name = line.substring(0, index).trim();
|
||||||
|
GameOption option = optionMap.get(name);
|
||||||
|
if (option != null) {
|
||||||
try {
|
try {
|
||||||
switch (name) {
|
String value = line.substring(index + 1).trim();
|
||||||
case "BeatmapDirectory":
|
option.read(value);
|
||||||
beatmapDir = new File(value);
|
|
||||||
break;
|
|
||||||
case "OSZDirectory":
|
|
||||||
oszDir = new File(value);
|
|
||||||
break;
|
|
||||||
case "ScreenshotDirectory":
|
|
||||||
screenshotDir = new File(value);
|
|
||||||
break;
|
|
||||||
case "ReplayDirectory":
|
|
||||||
replayDir = new File(value);
|
|
||||||
break;
|
|
||||||
case "Skin":
|
|
||||||
skinDir = new File(value);
|
|
||||||
break;
|
|
||||||
case "ThemeSong":
|
|
||||||
themeString = value;
|
|
||||||
break;
|
|
||||||
case "Port":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i > 0 && i <= 65535)
|
|
||||||
port = i;
|
|
||||||
break;
|
|
||||||
case "ScreenResolution":
|
|
||||||
try {
|
|
||||||
Resolution res = Resolution.valueOf(String.format("RES_%s", value.replace('x', '_')));
|
|
||||||
resolution = res;
|
|
||||||
} catch (IllegalArgumentException e) {}
|
|
||||||
break;
|
|
||||||
// case "Fullscreen":
|
|
||||||
// GameOption.FULLSCREEN.setValue(Boolean.parseBoolean(value));
|
|
||||||
// break;
|
|
||||||
case "FrameSync":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
for (int j = 0; j < targetFPS.length; j++) {
|
|
||||||
if (i == targetFPS[j])
|
|
||||||
targetFPSindex = j;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "ScreenshotFormat":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i < screenshotFormat.length)
|
|
||||||
screenshotFormatIndex = i;
|
|
||||||
break;
|
|
||||||
case "FpsCounter":
|
|
||||||
GameOption.SHOW_FPS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "ShowUnicode":
|
|
||||||
GameOption.SHOW_UNICODE.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "NewCursor":
|
|
||||||
GameOption.NEW_CURSOR.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "DynamicBackground":
|
|
||||||
GameOption.DYNAMIC_BACKGROUND.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "LoadVerbose":
|
|
||||||
GameOption.LOAD_VERBOSE.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "VolumeUniversal":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i <= 100)
|
|
||||||
GameOption.MASTER_VOLUME.setValue(i);
|
|
||||||
break;
|
|
||||||
case "VolumeMusic":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i <= 100)
|
|
||||||
GameOption.MUSIC_VOLUME.setValue(i);
|
|
||||||
break;
|
|
||||||
case "VolumeEffect":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i <= 100)
|
|
||||||
GameOption.EFFECT_VOLUME.setValue(i);
|
|
||||||
break;
|
|
||||||
case "VolumeHitSound":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i <= 100)
|
|
||||||
GameOption.HITSOUND_VOLUME.setValue(i);
|
|
||||||
break;
|
|
||||||
case "Offset":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= -500 && i <= 500)
|
|
||||||
GameOption.MUSIC_OFFSET.setValue(i);
|
|
||||||
break;
|
|
||||||
case "DisableSound":
|
|
||||||
GameOption.DISABLE_SOUNDS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "keyOsuLeft":
|
|
||||||
setGameKeyLeft(Keyboard.getKeyIndex(value));
|
|
||||||
break;
|
|
||||||
case "keyOsuRight":
|
|
||||||
setGameKeyRight(Keyboard.getKeyIndex(value));
|
|
||||||
break;
|
|
||||||
case "MouseDisableWheel":
|
|
||||||
GameOption.DISABLE_MOUSE_WHEEL.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "MouseDisableButtons":
|
|
||||||
GameOption.DISABLE_MOUSE_BUTTONS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "DimLevel":
|
|
||||||
i = Integer.parseInt(value);
|
|
||||||
if (i >= 0 && i <= 100)
|
|
||||||
GameOption.BACKGROUND_DIM.setValue(i);
|
|
||||||
break;
|
|
||||||
case "ForceDefaultPlayfield":
|
|
||||||
GameOption.FORCE_DEFAULT_PLAYFIELD.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "IgnoreBeatmapSkins":
|
|
||||||
GameOption.IGNORE_BEATMAP_SKINS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "HitLighting":
|
|
||||||
GameOption.SHOW_HIT_LIGHTING.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "ComboBurst":
|
|
||||||
GameOption.SHOW_COMBO_BURSTS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "PerfectHit":
|
|
||||||
GameOption.SHOW_PERFECT_HIT.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "FollowPoints":
|
|
||||||
GameOption.SHOW_FOLLOW_POINTS.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "ScoreMeter":
|
|
||||||
GameOption.SHOW_HIT_ERROR_BAR.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "LoadHDImages":
|
|
||||||
GameOption.LOAD_HD_IMAGES.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
case "FixedCS":
|
|
||||||
GameOption.FIXED_CS.setValue((int) (Float.parseFloat(value) * 10f));
|
|
||||||
break;
|
|
||||||
case "FixedHP":
|
|
||||||
GameOption.FIXED_HP.setValue((int) (Float.parseFloat(value) * 10f));
|
|
||||||
break;
|
|
||||||
case "FixedAR":
|
|
||||||
GameOption.FIXED_AR.setValue((int) (Float.parseFloat(value) * 10f));
|
|
||||||
break;
|
|
||||||
case "FixedOD":
|
|
||||||
GameOption.FIXED_OD.setValue((int) (Float.parseFloat(value) * 10f));
|
|
||||||
break;
|
|
||||||
case "Checkpoint":
|
|
||||||
setCheckpoint(Integer.parseInt(value));
|
|
||||||
break;
|
|
||||||
case "MenuMusic":
|
|
||||||
GameOption.ENABLE_THEME_SONG.setValue(Boolean.parseBoolean(value));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.warn(String.format("Format error in options file for line: '%s'.", line), e);
|
Log.warn(String.format("Format error in options file for line: '%s'.", line), e);
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -1105,88 +1275,12 @@ public class Options {
|
|||||||
writer.newLine();
|
writer.newLine();
|
||||||
|
|
||||||
// options
|
// options
|
||||||
writer.write(String.format("BeatmapDirectory = %s", getBeatmapDir().getAbsolutePath()));
|
for (GameOption option : GameOption.values()) {
|
||||||
writer.newLine();
|
writer.write(option.getDisplayName());
|
||||||
writer.write(String.format("OSZDirectory = %s", getOSZDir().getAbsolutePath()));
|
writer.write(" = ");
|
||||||
writer.newLine();
|
writer.write(option.write());
|
||||||
writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ThemeSong = %s", themeString));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("Port = %d", port));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ScreenResolution = %s", resolution.toString()));
|
|
||||||
writer.newLine();
|
|
||||||
// writer.write(String.format("Fullscreen = %b", isFullscreen()));
|
|
||||||
// writer.newLine();
|
|
||||||
writer.write(String.format("FrameSync = %d", targetFPS[targetFPSindex]));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("FpsCounter = %b", isFPSCounterEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ShowUnicode = %b", useUnicodeMetadata()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("NewCursor = %b", isNewCursorEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("DynamicBackground = %b", isDynamicBackgroundEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("LoadVerbose = %b", isLoadVerbose()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("VolumeUniversal = %d", GameOption.MASTER_VOLUME.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("VolumeMusic = %d", GameOption.MUSIC_VOLUME.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("VolumeEffect = %d", GameOption.EFFECT_VOLUME.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("VolumeHitSound = %d", GameOption.HITSOUND_VOLUME.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("Offset = %d", getMusicOffset()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("DisableSound = %b", isSoundDisabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("keyOsuLeft = %s", Keyboard.getKeyName(getGameKeyLeft())));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("keyOsuRight = %s", Keyboard.getKeyName(getGameKeyRight())));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("MouseDisableWheel = %b", isMouseWheelDisabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("MouseDisableButtons = %b", isMouseDisabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("DimLevel = %d", GameOption.BACKGROUND_DIM.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ForceDefaultPlayfield = %b", isDefaultPlayfieldForced()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("IgnoreBeatmapSkins = %b", isBeatmapSkinIgnored()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("HitLighting = %b", isHitLightingEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ComboBurst = %b", isComboBurstEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("PerfectHit = %b", isPerfectHitBurstEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("FollowPoints = %b", isFollowPointEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("ScoreMeter = %b", isHitErrorBarEnabled()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("LoadHDImages = %b", loadHDImages()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format(Locale.US, "FixedCS = %.1f", getFixedCS()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format(Locale.US, "FixedHP = %.1f", getFixedHP()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format(Locale.US, "FixedAR = %.1f", getFixedAR()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format(Locale.US, "FixedOD = %.1f", getFixedOD()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("Checkpoint = %d", GameOption.CHECKPOINT.getIntegerValue()));
|
|
||||||
writer.newLine();
|
|
||||||
writer.write(String.format("MenuMusic = %b", isThemeSongEnabled()));
|
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
|
}
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false);
|
ErrorHandler.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 (skinDir != null) {
|
||||||
|
File skinWAV = new File(skinDir, wav), skinMP3 = new File(skinDir, mp3);
|
||||||
if (skinWAV.isFile())
|
if (skinWAV.isFile())
|
||||||
return skinWAV.getAbsolutePath();
|
return skinWAV.getAbsolutePath();
|
||||||
if (skinMP3.isFile())
|
if (skinMP3.isFile())
|
||||||
return skinMP3.getAbsolutePath();
|
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,17 +293,21 @@ public class SoundController {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// play all sounds
|
// play all sounds
|
||||||
|
if (hitSound == HitObject.SOUND_NORMAL || Options.getSkin().isLayeredHitSounds()) {
|
||||||
HitSound.setSampleSet(sampleSet);
|
HitSound.setSampleSet(sampleSet);
|
||||||
playClip(HitSound.NORMAL.getClip(), volume, null);
|
playClip(HitSound.NORMAL.getClip(), volume, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitSound != HitObject.SOUND_NORMAL) {
|
||||||
HitSound.setSampleSet(additionSampleSet);
|
HitSound.setSampleSet(additionSampleSet);
|
||||||
if ((hitSound & OsuHitObject.SOUND_WHISTLE) > 0)
|
if ((hitSound & HitObject.SOUND_WHISTLE) > 0)
|
||||||
playClip(HitSound.WHISTLE.getClip(), volume, null);
|
playClip(HitSound.WHISTLE.getClip(), volume, null);
|
||||||
if ((hitSound & OsuHitObject.SOUND_FINISH) > 0)
|
if ((hitSound & HitObject.SOUND_FINISH) > 0)
|
||||||
playClip(HitSound.FINISH.getClip(), volume, null);
|
playClip(HitSound.FINISH.getClip(), volume, null);
|
||||||
if ((hitSound & OsuHitObject.SOUND_CLAP) > 0)
|
if ((hitSound & HitObject.SOUND_CLAP) > 0)
|
||||||
playClip(HitSound.CLAP.getClip(), volume, null);
|
playClip(HitSound.CLAP.getClip(), volume, null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a hit sound.
|
* Plays a hit sound.
|
||||||
|
|||||||
455
src/itdelatrisu/opsu/beatmap/Beatmap.java
Normal 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])));
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java
Normal 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(); }
|
||||||
|
}
|
||||||
741
src/itdelatrisu/opsu/beatmap/BeatmapParser.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/itdelatrisu/opsu/beatmap/BeatmapSet.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
504
src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
Normal 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); }
|
||||||
|
}
|
||||||
131
src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
540
src/itdelatrisu/opsu/beatmap/HitObject.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
590
src/itdelatrisu/opsu/db/BeatmapDB.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,10 +240,15 @@ public class DownloadNode {
|
|||||||
* @see #getDownload()
|
* @see #getDownload()
|
||||||
*/
|
*/
|
||||||
public void createDownload(DownloadServer server) {
|
public void createDownload(DownloadServer server) {
|
||||||
if (download == null) {
|
if (download != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
String url = server.getDownloadURL(beatmapSetID);
|
||||||
|
if (url == null)
|
||||||
|
return;
|
||||||
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
|
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
|
||||||
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
|
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
|
||||||
this.download = new Download(server.getURL(beatmapSetID), path, rename);
|
this.download = new Download(url, path, rename);
|
||||||
download.setListener(new DownloadListener() {
|
download.setListener(new DownloadListener() {
|
||||||
@Override
|
@Override
|
||||||
public void completed() {
|
public void completed() {
|
||||||
@@ -255,7 +261,6 @@ public class DownloadNode {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the associated download object, or null if none.
|
* Returns the associated download object, or null if none.
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
JSONObject json = null;
|
|
||||||
if (s != null) {
|
|
||||||
try {
|
try {
|
||||||
json = new JSONObject(s);
|
// make string parseable by SimpleDateFormat
|
||||||
} catch (JSONException e) {
|
int index = s.lastIndexOf(':');
|
||||||
ErrorHandler.error("Failed to create JSON object.", e, true);
|
if (index == -1)
|
||||||
|
return s;
|
||||||
|
String str = new StringBuilder(s).deleteCharAt(index).toString();
|
||||||
|
|
||||||
|
DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||||
|
Date d = f.parse(str);
|
||||||
|
DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
|
||||||
|
return fmt.format(d);
|
||||||
|
} catch (StringIndexOutOfBoundsException | ParseException e) {
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
139
src/itdelatrisu/opsu/downloads/servers/HexideServer.java
Normal 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; }
|
||||||
|
}
|
||||||
151
src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
||||||
|
if (!overlayAboveNumber)
|
||||||
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
72
src/itdelatrisu/opsu/objects/GameObject.java
Normal 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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
77
src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 Catmull–Rom spline.
|
||||||
|
* (Currently not technically Centripetal Catmull–Rom.)
|
||||||
|
* http://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
|
||||||
|
*
|
||||||
|
* @author fluddokt (https://github.com/fluddokt)
|
||||||
|
*/
|
||||||
|
public class CentripetalCatmullRom extends CurveType {
|
||||||
|
/** The time values of the Catmull curve. */
|
||||||
|
private float [] time;
|
||||||
|
|
||||||
|
/** The control points of the Catmull curve. */
|
||||||
|
private Vec2f[] points;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param points the control points of the curve
|
||||||
|
* @throws SlickException
|
||||||
|
*/
|
||||||
|
protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
|
||||||
|
if (points.length != 4)
|
||||||
|
throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
|
||||||
|
|
||||||
|
this.points = points;
|
||||||
|
time = new float[4];
|
||||||
|
time[0] = 0;
|
||||||
|
float approxLength = 0;
|
||||||
|
for (int i = 1; i < 4; i++) {
|
||||||
|
float len = 0;
|
||||||
|
if (i > 0)
|
||||||
|
len = points[i].cpy().sub(points[i - 1]).len();
|
||||||
|
if (len <= 0)
|
||||||
|
len += 0.0001f;
|
||||||
|
approxLength += len;
|
||||||
|
// time[i] = (float) Math.sqrt(len) + time[i-1];// ^(0.5)
|
||||||
|
time[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(approxLength / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vec2f pointAt(float t) {
|
||||||
|
t = t * (time[2] - time[1]) + time[1];
|
||||||
|
|
||||||
|
Vec2f A1 = points[0].cpy().scale((time[1] - t) / (time[1] - time[0]))
|
||||||
|
.add(points[1].cpy().scale((t - time[0]) / (time[1] - time[0])));
|
||||||
|
Vec2f A2 = points[1].cpy().scale((time[2] - t) / (time[2] - time[1]))
|
||||||
|
.add(points[2].cpy().scale((t - time[1]) / (time[2] - time[1])));
|
||||||
|
Vec2f A3 = points[2].cpy().scale((time[3] - t) / (time[3] - time[2]))
|
||||||
|
.add(points[3].cpy().scale((t - time[2]) / (time[3] - time[2])));
|
||||||
|
|
||||||
|
Vec2f B1 = A1.cpy().scale((time[2] - t) / (time[2] - time[0]))
|
||||||
|
.add(A2.cpy().scale((t - time[0]) / (time[2] - time[0])));
|
||||||
|
Vec2f B2 = A2.cpy().scale((time[3] - t) / (time[3] - time[1]))
|
||||||
|
.add(A3.cpy().scale((t - time[1]) / (time[3] - time[1])));
|
||||||
|
|
||||||
|
Vec2f C = B1.cpy().scale((time[2] - t) / (time[2] - time[1]))
|
||||||
|
.add(B2.cpy().scale((t - time[1]) / (time[2] - time[1])));
|
||||||
|
|
||||||
|
return C;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/itdelatrisu/opsu/objects/curves/CurveType.java
Normal 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; }
|
||||||
|
}
|
||||||
142
src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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); }
|
||||||
}
|
}
|
||||||
|
|||||||
451
src/itdelatrisu/opsu/render/CurveRenderState.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/itdelatrisu/opsu/render/FrameBufferCache.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/itdelatrisu/opsu/render/Rendertarget.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
93
src/itdelatrisu/opsu/replay/PlaybackSpeed.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
|||||||
374
src/itdelatrisu/opsu/skins/Skin.java
Normal 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; }
|
||||||
|
}
|
||||||
299
src/itdelatrisu/opsu/skins/SkinLoader.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,11 +553,15 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
} else {
|
} else {
|
||||||
// start download
|
// start download
|
||||||
if (!DownloadList.get().contains(node.getID())) {
|
if (!DownloadList.get().contains(node.getID())) {
|
||||||
|
node.createDownload(SERVERS[serverIndex]);
|
||||||
|
if (node.getDownload() == null)
|
||||||
|
UI.sendBarNotification("The download could not be started.");
|
||||||
|
else {
|
||||||
DownloadList.get().addNode(node);
|
DownloadList.get().addNode(node);
|
||||||
node.createDownload(server);
|
|
||||||
node.getDownload().start();
|
node.getDownload().start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// set focus
|
// set focus
|
||||||
focusResult = index;
|
focusResult = index;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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())
|
// watching replay
|
||||||
|
if (isReplay || GameMod.AUTO.isActive()) {
|
||||||
|
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// watching replay
|
// skip button
|
||||||
if (isReplay) {
|
if (skipButton.contains(x, y))
|
||||||
// only allow skip button
|
|
||||||
if (button != Input.MOUSE_MIDDLE_BUTTON && 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// reload sounds if skin changed
|
||||||
|
if (newSkin) {
|
||||||
|
thread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// TODO: only reload each sound if actually needed?
|
||||||
|
SoundController.init();
|
||||||
|
|
||||||
finished = true;
|
finished = true;
|
||||||
} else {
|
thread = null;
|
||||||
// load resources in a new thread
|
}
|
||||||
|
};
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
309
src/itdelatrisu/opsu/ui/Cursor.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
506
src/itdelatrisu/opsu/ui/UI.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
1568
src/org/newdawn/slick/Input.java
Normal 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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||