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

Conflicts:
	src/itdelatrisu/opsu/ScoreData.java
	src/itdelatrisu/opsu/downloads/DownloadNode.java
	src/itdelatrisu/opsu/states/DownloadsMenu.java
	src/itdelatrisu/opsu/states/SongMenu.java
This commit is contained in:
fd
2015-09-14 19:32:18 -04:00
102 changed files with 6168 additions and 1857 deletions

View File

@@ -21,6 +21,7 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.render.CurveRenderState;
@@ -125,7 +126,7 @@ public class Container extends AppGameContainer {
// reset image references
GameImage.clearReferences();
GameData.Grade.clearReferences();
Beatmap.getBackgroundImageCache().clear();
Beatmap.clearBackgroundImageCache();
// prevent loading tracks from re-initializing OpenAL
MusicController.reset();
@@ -136,6 +137,11 @@ public class Container extends AppGameContainer {
// delete OpenGL objects involved in the Curve rendering
CurveRenderState.shutdown();
// destroy watch service
if (!Options.isWatchServiceEnabled())
BeatmapWatchService.destroy();
BeatmapWatchService.removeListeners();
}
@Override

View File

@@ -20,8 +20,10 @@ package itdelatrisu.opsu;
import java.awt.Cursor;
import java.awt.Desktop;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Properties;
@@ -44,12 +46,13 @@ public class ErrorHandler {
/** Error popup description text. */
private static final String
desc = "opsu! has encountered an error.",
descR = "opsu! has encountered an error. Please report this!";
descReport = "opsu! has encountered an error. Please report this!";
/** Error popup button options. */
private static final String[]
options = {"View Error Log", "Close"},
optionsR = {"Send Report", "View Error Log", "Close"};
optionsLog = {"View Error Log", "Close"},
optionsReport = {"Send Report", "Close"},
optionsLogReport = {"Send Report", "View Error Log", "Close"};
/** Text area for Exception. */
private static final JTextArea textArea = new JTextArea(7, 30);
@@ -59,6 +62,7 @@ public class ErrorHandler {
textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
textArea.setTabSize(2);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
}
/** Scroll pane holding JTextArea. */
@@ -67,7 +71,7 @@ public class ErrorHandler {
/** Error popup objects. */
private static final Object[]
message = { desc, scroll },
messageR = { descR, scroll };
messageReport = { descReport, scroll };
// This class should not be instantiated.
private ErrorHandler() {}
@@ -107,72 +111,112 @@ public class ErrorHandler {
// display popup
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Desktop desktop = null;
boolean isBrowseSupported = false, isOpenSupported = false;
if (Desktop.isDesktopSupported()) {
// try to open the log file and/or issues webpage
if (report) {
// ask to report the error
int n = JOptionPane.showOptionDialog(null, messageR, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsR, optionsR[2]);
if (n == 0) {
// auto-fill debug information
String issueTitle = (error != null) ? error : e.getMessage();
StringBuilder sb = new StringBuilder();
Properties props = new Properties();
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
String version = props.getProperty("version");
if (version != null && !version.equals("${pom.version}")) {
sb.append("**Version:** ");
sb.append(version);
sb.append('\n');
}
String timestamp = props.getProperty("build.date");
if (timestamp != null &&
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
sb.append("**Build date:** ");
sb.append(timestamp);
sb.append('\n');
}
sb.append("**OS:** ");
sb.append(System.getProperty("os.name"));
sb.append(" (");
sb.append(System.getProperty("os.arch"));
sb.append(")\n");
sb.append("**JRE:** ");
sb.append(System.getProperty("java.version"));
sb.append('\n');
if (error != null) {
sb.append("**Error:** `");
sb.append(error);
sb.append("`\n");
}
if (trace != null) {
sb.append("**Stack trace:**");
sb.append("\n```\n");
sb.append(trace);
sb.append("```");
}
URI uri = URI.create(String.format(Options.ISSUES_URL,
URLEncoder.encode(issueTitle, "UTF-8"),
URLEncoder.encode(sb.toString(), "UTF-8")));
Desktop.getDesktop().browse(uri);
} else if (n == 1)
Desktop.getDesktop().open(Options.LOG_FILE);
} else {
// don't report the error
desktop = Desktop.getDesktop();
isBrowseSupported = desktop.isSupported(Desktop.Action.BROWSE);
isOpenSupported = desktop.isSupported(Desktop.Action.OPEN);
}
if (desktop != null && (isOpenSupported || (report && isBrowseSupported))) { // try to open the log file and/or issues webpage
if (report && isBrowseSupported) { // ask to report the error
if (isOpenSupported) { // also ask to open the log
int n = JOptionPane.showOptionDialog(null, messageReport, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsLogReport, optionsLogReport[2]);
if (n == 0)
desktop.browse(getIssueURI(error, e, trace));
else if (n == 1)
desktop.open(Options.LOG_FILE);
} else { // only ask to report the error
int n = JOptionPane.showOptionDialog(null, message, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsReport, optionsReport[1]);
if (n == 0)
desktop.browse(getIssueURI(error, e, trace));
}
} else { // don't report the error
int n = JOptionPane.showOptionDialog(null, message, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, options, options[1]);
null, optionsLog, optionsLog[1]);
if (n == 0)
Desktop.getDesktop().open(Options.LOG_FILE);
desktop.open(Options.LOG_FILE);
}
} else {
// display error only
JOptionPane.showMessageDialog(null, report ? messageR : message,
} else { // display error only
JOptionPane.showMessageDialog(null, report ? messageReport : message,
title, JOptionPane.ERROR_MESSAGE);
}
} catch (Exception e1) {
Log.error("Error opening crash popup.", e1);
Log.error("An error occurred in the crash popup.", e1);
}
}
/**
* Returns the issue reporting URI.
* This will auto-fill the report with the relevant information if possible.
* @param error a description of the error
* @param e the exception causing the error
* @param trace the stack trace
* @return the created URI
*/
private static URI getIssueURI(String error, Throwable e, String trace) {
// generate report information
String issueTitle = (error != null) ? error : e.getMessage();
StringBuilder sb = new StringBuilder();
try {
// read version and build date from version file, if possible
Properties props = new Properties();
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
String version = props.getProperty("version");
if (version != null && !version.equals("${pom.version}")) {
sb.append("**Version:** ");
sb.append(version);
String hash = Utils.getGitHash();
if (hash != null) {
sb.append(" (");
sb.append(hash.substring(0, 12));
sb.append(')');
}
sb.append('\n');
}
String timestamp = props.getProperty("build.date");
if (timestamp != null &&
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
sb.append("**Build date:** ");
sb.append(timestamp);
sb.append('\n');
}
} catch (IOException e1) {
Log.warn("Could not read version file.", e1);
}
sb.append("**OS:** ");
sb.append(System.getProperty("os.name"));
sb.append(" (");
sb.append(System.getProperty("os.arch"));
sb.append(")\n");
sb.append("**JRE:** ");
sb.append(System.getProperty("java.version"));
sb.append('\n');
if (error != null) {
sb.append("**Error:** `");
sb.append(error);
sb.append("`\n");
}
if (trace != null) {
sb.append("**Stack trace:**");
sb.append("\n```\n");
sb.append(trace);
sb.append("```");
}
// return auto-filled URI
try {
return URI.create(String.format(Options.ISSUES_URL,
URLEncoder.encode(issueTitle, "UTF-8"),
URLEncoder.encode(sb.toString(), "UTF-8")));
} catch (UnsupportedEncodingException e1) {
Log.warn("URLEncoder failed to encode the auto-filled issue report URL.");
return URI.create(String.format(Options.ISSUES_URL, "", ""));
}
}
}

View File

@@ -26,8 +26,12 @@ import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.Date;
@@ -87,7 +91,7 @@ public class GameData {
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
/** GameImages associated with this grade (large and small sizes). */
private GameImage large, small;
private final GameImage large, small;
/** Large-size image scaled for use in song menu. */
private Image menuImage;
@@ -129,7 +133,7 @@ public class GameData {
return menuImage;
Image img = getSmallImage();
if (!small.hasSkinImage()) // save default image only
if (!small.hasBeatmapSkinImage()) // save default image only
this.menuImage = img;
return img;
}
@@ -199,14 +203,14 @@ public class GameData {
*/
private class HitErrorInfo {
/** The correct hit time. */
private int time;
private final int time;
/** The coordinates of the hit. */
@SuppressWarnings("unused")
private int x, y;
private final int x, y;
/** The difference between the correct and actual hit times. */
private int timeDiff;
private final int timeDiff;
/**
* Constructor.
@@ -232,29 +236,32 @@ public class GameData {
/** Hit result helper class. */
private class HitObjectResult {
/** Object start time. */
public int time;
public final int time;
/** Hit result. */
public int result;
public final int result;
/** Object coordinates. */
public float x, y;
public final float x, y;
/** Combo color. */
public Color color;
public final Color color;
/** The type of the hit object. */
public HitObjectType hitResultType;
public final HitObjectType hitResultType;
/** Slider curve. */
public final Curve curve;
/** Whether or not to expand when animating. */
public final boolean expand;
/** Whether or not to hide the hit result. */
public final boolean hideResult;
/** Alpha level (for fading out). */
public float alpha = 1f;
/** Slider curve. */
public Curve curve;
/** Whether or not to expand when animating. */
public boolean expand;
/**
* Constructor.
* @param time the result's starting track position
@@ -262,11 +269,13 @@ public class GameData {
* @param x the center x coordinate
* @param y the center y coordinate
* @param color the color of the hit object
* @param hitResultType the hit object type
* @param curve the slider curve (or null if not applicable)
* @param expand whether or not the hit result animation should expand (if applicable)
* @param hideResult whether or not to hide the hit result (but still show the other animations)
*/
public HitObjectResult(int time, int result, float x, float y, Color color,
HitObjectType hitResultType, Curve curve, boolean expand) {
HitObjectType hitResultType, Curve curve, boolean expand, boolean hideResult) {
this.time = time;
this.result = result;
this.x = x;
@@ -275,6 +284,7 @@ public class GameData {
this.hitResultType = hitResultType;
this.curve = curve;
this.expand = expand;
this.hideResult = hideResult;
}
}
@@ -316,7 +326,7 @@ public class GameData {
private Replay replay;
/** Whether this object is used for gameplay (true) or score viewing (false). */
private boolean gameplay;
private boolean isGameplay;
/** Container dimensions. */
private int width, height;
@@ -329,7 +339,7 @@ public class GameData {
public GameData(int width, int height) {
this.width = width;
this.height = height;
this.gameplay = true;
this.isGameplay = true;
clear();
}
@@ -345,7 +355,7 @@ public class GameData {
public GameData(ScoreData s, int width, int height) {
this.width = width;
this.height = height;
this.gameplay = false;
this.isGameplay = false;
this.scoreData = s;
this.score = s.score;
@@ -400,8 +410,8 @@ public class GameData {
// gameplay-specific images
if (isGameplay()) {
// combo burst images
if (GameImage.COMBO_BURST.hasSkinImages() ||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
if (GameImage.COMBO_BURST.hasBeatmapSkinImages() ||
(!GameImage.COMBO_BURST.hasBeatmapSkinImage() && GameImage.COMBO_BURST.getImages() != null))
comboBurstImages = GameImage.COMBO_BURST.getImages();
else
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
@@ -474,6 +484,7 @@ public class GameData {
/**
* Sets the array of hit result offsets.
* @param hitResultOffset the time offset array (of size {@link #HIT_MAX})
*/
public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; }
@@ -614,7 +625,7 @@ public class GameData {
);
} else {
// lead-in time (yellow)
g.setColor(Utils.COLOR_YELLOW_ALPHA);
g.setColor(Colors.YELLOW_ALPHA);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
-90 + (int) (360f * trackPosition / firstObjectTime), -90
);
@@ -651,27 +662,27 @@ public class GameData {
float hitErrorY = height / uiScale - margin - 10;
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = hitErrorAlpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = hitErrorAlpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
Utils.COLOR_LIGHT_ORANGE.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_ORANGE);
Colors.BLACK_ALPHA.a = oldAlphaBlack;
Colors.LIGHT_ORANGE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_ORANGE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_ORANGE.a = 1f;
Utils.COLOR_LIGHT_GREEN.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_GREEN);
Colors.LIGHT_ORANGE.a = 1f;
Colors.LIGHT_GREEN.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_GREEN);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_GREEN.a = 1f;
Utils.COLOR_LIGHT_BLUE.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_BLUE);
Colors.LIGHT_GREEN.a = 1f;
Colors.LIGHT_BLUE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_BLUE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_BLUE.a = 1f;
Colors.LIGHT_BLUE.a = 1f;
white.a = hitErrorAlpha;
g.setColor(white);
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
@@ -830,16 +841,16 @@ public class GameData {
// header
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, 100 * uiScale);
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
float marginX = width * 0.01f, marginY = height * 0.002f;
Utils.FONT_LARGE.drawString(marginX, marginY,
Fonts.LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", beatmap.creator), Color.white);
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() + Fonts.MEDIUM.getLineHeight() - 10,
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
// mod icons
@@ -872,7 +883,7 @@ public class GameData {
}
// hit lighting
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.result != HIT_MISS &&
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
// TODO: add particle system
Image lighting = GameImage.LIGHTING.getImage();
@@ -885,47 +896,45 @@ public class GameData {
hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
float scale = (!hitResult.expand) ? 1f : Utils.easeOut(
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME
);
float alpha = Utils.easeOut(
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
1f, -1f, HITCIRCLE_FADE_TIME
);
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
float alpha = 1f - progress;
// slider curve
if (hitResult.curve != null) {
float oldWhiteAlpha = Utils.COLOR_WHITE_FADE.a;
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Utils.COLOR_WHITE_FADE.a = alpha;
Colors.WHITE_FADE.a = alpha;
hitResult.color.a = alpha;
hitResult.curve.draw(hitResult.color);
Utils.COLOR_WHITE_FADE.a = oldWhiteAlpha;
Colors.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);
if (!(hitResult.hitResultType == HitObjectType.CIRCLE && GameMod.HIDDEN.isActive())) {
// "hidden" mod: expanding animation for only circles not drawn
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 ||
if (!hitResult.hideResult && (
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
);
hitResult.curve != null)) {
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
float fadeProgress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME) / HITCIRCLE_TEXT_FADE_TIME);
float alpha = 1f - fadeProgress;
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
scaledHitResult.setAlpha(alpha);
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
@@ -1037,10 +1046,13 @@ public class GameData {
* or {@code Grade.NULL} if no objects have been processed.
*/
private Grade getGrade() {
boolean silver = (scoreData == null) ?
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) :
(scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0;
return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
silver
);
}
@@ -1179,7 +1191,7 @@ public class GameData {
switch (result) {
case HIT_SLIDER30:
hitValue = 30;
changeHealth(1f);
changeHealth(2f);
SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat),
@@ -1187,6 +1199,7 @@ public class GameData {
break;
case HIT_SLIDER10:
hitValue = 10;
changeHealth(1f);
SoundController.playHitSound(HitSound.SLIDERTICK);
break;
case HIT_MISS:
@@ -1204,7 +1217,7 @@ public class GameData {
if (!Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false));
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false, false));
}
fullObjectCount++;
}
@@ -1360,20 +1373,18 @@ public class GameData {
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
; // "relax" and "autopilot" mods: hide misses
else {
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand));
if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
return; // "relax" and "autopilot" mods: hide misses
// 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, hitResult, p[0], p[1], color, type, null, expand));
}
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
// sliders: add the other curve endpoint for the hit animation
if (curve != null) {
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
hitResultList.add(new HitObjectResult(time, hitResult, p.x, p.y, color, type, null, expand, hideResult));
}
}
@@ -1460,7 +1471,13 @@ public class GameData {
* Returns whether or not this object is used for gameplay.
* @return true if gameplay, false if score viewing
*/
public boolean isGameplay() { return gameplay; }
public boolean isGameplay() { return isGameplay; }
/**
* Sets whether or not this object is used for gameplay.
* @param gameplay true if gameplay, false if score viewing
*/
public void setGameplay(boolean gameplay) { this.isGameplay = gameplay; }
/**
* Adds the hit into the list of hit error information.

View File

@@ -18,6 +18,8 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.ui.Fonts;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -35,8 +37,8 @@ public enum GameImage {
CURSOR ("cursor", "png"),
CURSOR_MIDDLE ("cursormiddle", "png"),
CURSOR_TRAIL ("cursortrail", "png"),
CURSOR_OLD ("cursor2", "png", false, false),
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false),
CURSOR_OLD ("cursor2", "png", false, false), // custom
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false), // custom
// Game
SECTION_PASS ("section-pass", "png"),
@@ -251,14 +253,14 @@ public enum GameImage {
MENU_MUSICNOTE ("music-note", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
return img.getScaledCopy(r, r);
}
},
MENU_LOADER ("loader", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
return img.getScaledCopy(r / 48f);
}
},
@@ -290,12 +292,25 @@ public enum GameImage {
MENU_BUTTON_MID ("button-middle", "png", false, false),
MENU_BUTTON_LEFT ("button-left", "png", false, false),
MENU_BUTTON_RIGHT ("button-right", "png", false, false),
STAR ("star", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.16f) / img.getHeight());
}
},
STAR2 ("star2", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.33f) / img.getHeight());
}
},
// Music Player Buttons
MUSIC_PLAY ("music-play", "png", false, false),
MUSIC_PAUSE ("music-pause", "png", false, false),
MUSIC_NEXT ("music-next", "png", false, false),
MUSIC_PREVIOUS ("music-previous", "png", false, false),
DOWNLOADS ("downloads", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
@@ -312,7 +327,7 @@ public enum GameImage {
DELETE ("delete", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int lineHeight = Utils.FONT_DEFAULT.getLineHeight();
int lineHeight = Fonts.DEFAULT.getLineHeight();
return img.getScaledCopy(lineHeight, lineHeight);
}
},
@@ -328,10 +343,16 @@ public enum GameImage {
return img.getScaledCopy((h / 17f) / img.getHeight());
}
},
BANG ("bang", "png", false, false) {
DOWNLOAD ("download", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return REPOSITORY.process_sub(img, w, h);
return img.getScaledCopy((h / 14f) / img.getHeight());
}
},
UPDATE ("update", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((h / 14f) / img.getHeight());
}
},
OPTIONS_BG ("options-background", "png|jpg", false, true) {
@@ -341,6 +362,8 @@ public enum GameImage {
return img.getScaledCopy(w, h);
}
},
CHEVRON_DOWN ("chevron-down", "png", false, false),
CHEVRON_RIGHT ("chevron-right", "png", false, false),
// TODO: ensure this image hasn't been modified (checksum?)
ALPHA_MAP ("alpha", "png", false, false);
@@ -351,22 +374,22 @@ public enum GameImage {
IMG_JPG = 2;
/** The file name. */
private String filename;
private final String filename;
/** The formatted file name string (for loading multiple images). */
private String filenameFormat;
/** Image file type. */
private byte type;
private final byte type;
/**
* Whether or not the image is skinnable by a beatmap.
* These images are typically related to gameplay.
*/
private boolean skinnable;
private final boolean beatmapSkinnable;
/** Whether or not to preload the image when the program starts. */
private boolean preload;
private final boolean preload;
/** The default image. */
private Image defaultImage;
@@ -374,6 +397,9 @@ public enum GameImage {
/** The default image array. */
private Image[] defaultImages;
/** Whether the image is currently skinned by a game skin. */
private boolean isSkinned = false;
/** The beatmap skin image (optional, temporary). */
private Image skinImage;
@@ -421,6 +447,7 @@ public enum GameImage {
for (GameImage img : GameImage.values()) {
img.defaultImage = img.skinImage = null;
img.defaultImages = img.skinImages = null;
img.isSkinned = false;
}
}
@@ -488,7 +515,7 @@ public enum GameImage {
}
/**
* Constructor for game-related images (skinnable and preloaded).
* Constructor for game-related images (beatmap-skinnable and preloaded).
* @param filename the image file name
* @param type the file types (separated by '|')
*/
@@ -497,7 +524,7 @@ public enum GameImage {
}
/**
* Constructor for an array of game-related images (skinnable and preloaded).
* Constructor for an array of game-related images (beatmap-skinnable and preloaded).
* @param filename the image file name
* @param filenameFormat the formatted file name string (for loading multiple images)
* @param type the file types (separated by '|')
@@ -511,21 +538,21 @@ public enum GameImage {
* Constructor for general images.
* @param filename the image file name
* @param type the file types (separated by '|')
* @param skinnable whether or not the image is skinnable
* @param beatmapSkinnable whether or not the image is beatmap-skinnable
* @param preload whether or not to preload the image
*/
GameImage(String filename, String type, boolean skinnable, boolean preload) {
GameImage(String filename, String type, boolean beatmapSkinnable, boolean preload) {
this.filename = filename;
this.type = getType(type);
this.skinnable = skinnable;
this.beatmapSkinnable = beatmapSkinnable;
this.preload = preload;
}
/**
* Returns whether or not the image is skinnable.
* @return true if skinnable
* Returns whether or not the image is beatmap-skinnable.
* @return true if beatmap-skinnable
*/
public boolean isSkinnable() { return skinnable; }
public boolean isBeatmapSkinnable() { return beatmapSkinnable; }
/**
* Returns whether or not to preload the image when the program starts.
@@ -535,7 +562,7 @@ public enum GameImage {
/**
* Returns the image associated with this resource.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
*/
public Image getImage() {
setDefaultImage();
@@ -556,7 +583,7 @@ public enum GameImage {
/**
* Returns the image array associated with this resource.
* The skin images takes priority over the default images.
* The beatmap skin images takes priority over the default images.
*/
public Image[] getImages() {
setDefaultImage();
@@ -565,7 +592,7 @@ public enum GameImage {
/**
* Sets the image associated with this resource to another image.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
* @param img the image to set
*/
public void setImage(Image img) {
@@ -577,7 +604,7 @@ public enum GameImage {
/**
* Sets an image associated with this resource to another image.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
* @param img the image to set
* @param index the index in the image array
*/
@@ -602,16 +629,26 @@ public enum GameImage {
// try to load multiple images
File skinDir = Options.getSkin().getDirectory();
if (filenameFormat != null) {
if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
((defaultImages = loadImageArray(null)) != null)) {
if (skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) {
isSkinned = true;
process();
return;
}
if ((defaultImages = loadImageArray(null)) != null) {
isSkinned = false;
process();
return;
}
}
// try to load a single image
if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
((defaultImage = loadImageSingle(null)) != null)) {
if (skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) {
isSkinned = true;
process();
return;
}
if ((defaultImage = loadImageSingle(null)) != null) {
isSkinned = false;
process();
return;
}
@@ -620,17 +657,17 @@ public enum GameImage {
}
/**
* Sets the associated skin image.
* Sets the associated beatmap skin image.
* If the path does not contain the image, the default image is used.
* @param dir the image directory to search
* @return true if a new skin image is loaded, false otherwise
*/
public boolean setSkinImage(File dir) {
public boolean setBeatmapSkinImage(File dir) {
if (dir == null)
return false;
// destroy the existing images, if any
destroySkinImage();
destroyBeatmapSkinImage();
// beatmap skins disabled
if (Options.isBeatmapSkinIgnored())
@@ -709,21 +746,27 @@ public enum GameImage {
}
/**
* Returns whether a skin image is currently loaded.
* @return true if skin image exists
* Returns whether the default image loaded is part of a game skin.
* @return true if a game skin image is loaded, false if the default image is loaded
*/
public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
public boolean hasGameSkinImage() { return isSkinned; }
/**
* Returns whether skin images are currently loaded.
* @return true if any skin image exists
* Returns whether a beatmap skin image is currently loaded.
* @return true if a beatmap skin image exists
*/
public boolean hasSkinImages() { return (skinImages != null); }
public boolean hasBeatmapSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
/**
* Destroys the associated skin image(s), if any.
* Returns whether beatmap skin images are currently loaded.
* @return true if any beatmap skin image exists
*/
public void destroySkinImage() {
public boolean hasBeatmapSkinImages() { return (skinImages != null); }
/**
* Destroys the associated beatmap skin image(s), if any.
*/
public void destroyBeatmapSkinImage() {
if (skinImage == null && skinImages == null)
return;
try {
@@ -740,7 +783,7 @@ public enum GameImage {
skinImages = null;
}
} catch (SlickException e) {
ErrorHandler.error(String.format("Failed to destroy skin images for '%s'.", this.name()), e, true);
ErrorHandler.error(String.format("Failed to destroy beatmap skin images for '%s'.", this.name()), e, true);
}
}

View File

@@ -18,7 +18,9 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.Arrays;
import java.util.Collections;
@@ -47,7 +49,7 @@ public enum GameMod {
"DoubleTime", "Zoooooooooom."),
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
// "Nightcore", "uguuuuuuuu"),
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false,
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f,
"Hidden", "Play with no approach circles and fading notes for a slight score advantage."),
FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f,
"Flashlight", "Restricted view area."),
@@ -55,9 +57,9 @@ public enum GameMod {
"Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"),
AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f,
"Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"),
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f,
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_C, 0.9f,
"SpunOut", "Spinners will be automatically completed."),
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, 1f,
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_V, 1f,
"Autoplay", "Watch a perfect automated play through the song.");
/** Mod categories. */
@@ -67,13 +69,13 @@ public enum GameMod {
SPECIAL (2, "Special", Color.white);
/** Drawing index. */
private int index;
private final int index;
/** Category name. */
private String name;
private final String name;
/** Text color. */
private Color color;
private final Color color;
/** The coordinates of the category. */
private float x, y;
@@ -96,10 +98,10 @@ public enum GameMod {
* @param height the container height
*/
public void init(int width, int height) {
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
float offsetY = GameImage.MOD_EASY.getImage().getHeight() * 1.5f;
this.x = width / 30f;
this.y = multY + Utils.FONT_LARGE.getLineHeight() * 3f + offsetY * index;
this.y = multY + Fonts.LARGE.getLineHeight() * 3f + offsetY * index;
}
/**
@@ -124,37 +126,34 @@ public enum GameMod {
}
/** The category for the mod. */
private Category category;
private final Category category;
/** The index in the category (for positioning). */
private int categoryIndex;
private final int categoryIndex;
/** The file name of the mod image. */
private GameImage image;
private final GameImage image;
/** The abbreviation for the mod. */
private String abbrev;
private final String abbrev;
/**
* Bit value associated with the mod.
* See the osu! API: https://github.com/peppy/osu-api/wiki#mods
*/
private int bit;
private final int bit;
/** The shortcut key associated with the mod. */
private int key;
private final int key;
/** The score multiplier. */
private float multiplier;
/** Whether or not the mod is implemented. */
private boolean implemented;
private final float multiplier;
/** The name of the mod. */
private String name;
private final String name;
/** The description of the mod. */
private String description;
private final String description;
/** Whether or not this mod is active. */
private boolean active = false;
@@ -192,13 +191,15 @@ public enum GameMod {
c.init(width, height);
// create buttons
float baseX = Category.EASY.getX() + Utils.FONT_LARGE.getWidth(Category.EASY.getName()) * 1.25f;
float baseX = Category.EASY.getX() + Fonts.LARGE.getWidth(Category.EASY.getName()) * 1.25f;
float offsetX = GameImage.MOD_EASY.getImage().getWidth() * 2.1f;
for (GameMod mod : GameMod.values()) {
Image img = mod.image.getImage();
mod.button = new MenuButton(img,
baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f,
mod.category.getY());
mod.button.setHoverAnimationDuration(300);
mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
mod.button.setHoverExpand(1.2f);
mod.button.setHoverRotate(10f);
@@ -309,24 +310,6 @@ public enum GameMod {
*/
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
int bit, int key, float multiplier, String name, String description) {
this(category, categoryIndex, image, abbrev, bit, key, multiplier, true, name, description);
}
/**
* Constructor.
* @param category the category for the mod
* @param categoryIndex the index in the category
* @param image the GameImage
* @param abbrev the two-letter abbreviation
* @param bit the bit
* @param key the shortcut key
* @param multiplier the score multiplier
* @param implemented whether the mod is implemented
* @param name the name
* @param description the description
*/
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
int bit, int key, float multiplier, boolean implemented, String name, String description) {
this.category = category;
this.categoryIndex = categoryIndex;
this.image = image;
@@ -334,7 +317,6 @@ public enum GameMod {
this.bit = bit;
this.key = key;
this.multiplier = multiplier;
this.implemented = implemented;
this.name = name;
this.description = description;
}
@@ -376,20 +358,11 @@ public enum GameMod {
*/
public String getDescription() { return description; }
/**
* Returns whether or not the mod is implemented.
* @return true if implemented
*/
public boolean isImplemented() { return implemented; }
/**
* Toggles the active status of the mod.
* @param checkInverse if true, perform checks for mutual exclusivity
*/
public void toggle(boolean checkInverse) {
if (!implemented)
return;
active = !active;
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
@@ -446,14 +419,7 @@ public enum GameMod {
/**
* Draws the game mod.
*/
public void draw() {
if (!implemented) {
button.getImage().setAlpha(0.2f);
button.draw();
button.getImage().setAlpha(1f);
} else
button.draw();
}
public void draw() { button.draw(); }
/**
* Checks if the coordinates are within the image bounds.

View File

@@ -0,0 +1,104 @@
/*
* 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Native loader, based on the JarSplice launcher.
*
* @author http://ninjacave.com
*/
public class NativeLoader {
/** The directory to unpack natives to. */
private final File nativeDir;
/**
* Constructor.
* @param dir the directory to unpack natives to
*/
public NativeLoader(File dir) {
nativeDir = dir;
}
/**
* Unpacks natives for the current operating system to the natives directory.
* @throws IOException if an I/O exception occurs
*/
public void loadNatives() throws IOException {
if (!nativeDir.exists())
nativeDir.mkdir();
JarFile jarFile = Utils.getJarFile();
if (jarFile == null)
return;
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry e = entries.nextElement();
if (e == null)
break;
File f = new File(nativeDir, e.getName());
if (isNativeFile(e.getName()) && !e.isDirectory() && e.getName().indexOf('/') == -1 && !f.exists()) {
InputStream in = jarFile.getInputStream(jarFile.getEntry(e.getName()));
OutputStream out = new FileOutputStream(f);
byte[] buffer = new byte[65536];
int bufferSize;
while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1)
out.write(buffer, 0, bufferSize);
in.close();
out.close();
}
}
jarFile.close();
}
/**
* Returns whether the given file name is a native file for the current operating system.
* @param entryName the file name
* @return true if the file is a native that should be loaded, false otherwise
*/
private boolean isNativeFile(String entryName) {
String osName = System.getProperty("os.name");
String name = entryName.toLowerCase();
if (osName.startsWith("Win")) {
if (name.endsWith(".dll"))
return true;
} else if (osName.startsWith("Linux")) {
if (name.endsWith(".so"))
return true;
} else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) {
if (name.endsWith(".dylib") || name.endsWith(".jnilib"))
return true;
}
return false;
}
}

View File

@@ -38,10 +38,14 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition;
@@ -116,16 +120,43 @@ public class Opsu extends StateBasedGame {
// only allow a single instance
try {
SERVER_SOCKET = new ServerSocket(Options.getPort());
SERVER_SOCKET = new ServerSocket(Options.getPort(), 1, InetAddress.getLocalHost());
} catch (UnknownHostException e) {
// shouldn't happen
} catch (IOException e) {
ErrorHandler.error(String.format("Another program is already running on port %d.", Options.getPort()), e, false);
ErrorHandler.error(String.format(
"opsu! could not be launched for one of these reasons:\n" +
"- An instance of opsu! is already running.\n" +
"- Another program is bound to port %d. " +
"You can change the port opsu! uses by editing the \"Port\" field in the configuration file.",
Options.getPort()), null, false);
System.exit(1);
}
// set path for lwjgl natives - NOT NEEDED if using JarSplice
File nativeDir = new File("./target/natives/");
if (nativeDir.isDirectory())
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
File nativeDir;
if (!Utils.isJarRunning() && (
(nativeDir = new File("./target/natives/")).isDirectory() ||
(nativeDir = new File("./build/natives/")).isDirectory()))
;
else {
nativeDir = Options.NATIVE_DIR;
try {
new NativeLoader(nativeDir).loadNatives();
} catch (IOException e) {
Log.error("Error loading natives.", e);
}
}
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
System.setProperty("java.library.path", nativeDir.getAbsolutePath());
try {
// Workaround for "java.library.path" property being read-only.
// http://stackoverflow.com/a/24988095
Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
fieldSysPath.setAccessible(true);
fieldSysPath.set(null, null);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
Log.warn("Failed to set 'sys_paths' field.", e);
}
// set the resource paths
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
@@ -142,16 +173,21 @@ public class Opsu extends StateBasedGame {
Updater.get().setUpdateInfo(args[0], args[1]);
// check for updates
new Thread() {
@Override
public void run() {
try {
Updater.get().checkForUpdates();
} catch (IOException e) {
Log.warn("Check for updates failed.", e);
if (!Options.isUpdaterDisabled()) {
new Thread() {
@Override
public void run() {
try {
Updater.get().checkForUpdates();
} catch (IOException e) {
Log.warn("Check for updates failed.", e);
}
}
}
}.start();
}.start();
}
// disable jinput
Input.disableControllers();
// start the game
try {
@@ -190,30 +226,28 @@ public class Opsu extends StateBasedGame {
SongMenu songMenu = (SongMenu) this.getState(Opsu.STATE_SONGMENU);
if (id == STATE_GAMERANKING) {
GameData data = ((GameRanking) this.getState(Opsu.STATE_GAMERANKING)).getGameData();
if (data != null && data.isGameplay()) {
songMenu.resetGameDataOnLoad();
if (data != null && data.isGameplay())
songMenu.resetTrackOnLoad();
}
} else {
songMenu.resetGameDataOnLoad();
if (id == STATE_GAME) {
MusicController.pause();
MusicController.resume();
} else
songMenu.resetTrackOnLoad();
}
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
songMenu.resetGameDataOnLoad();
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return false;
}
// show confirmation dialog if any downloads are active
if (DownloadList.get().hasActiveDownloads() &&
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
return false;
if (Updater.get().getStatus() == Updater.Status.UPDATE_DOWNLOADING &&
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
return false;
return true;
@@ -248,7 +282,7 @@ public class Opsu extends StateBasedGame {
// 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)
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);

View File

@@ -22,6 +22,7 @@ import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.skins.SkinLoader;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.io.BufferedReader;
@@ -37,6 +38,9 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.lwjgl.input.Keyboard;
import org.newdawn.slick.GameContainer;
@@ -51,12 +55,18 @@ import org.newdawn.slick.util.ResourceLoader;
* Handles all user options.
*/
public class Options {
/** Whether to use XDG directories. */
public static final boolean USE_XDG = checkXDGFlag();
/** The config directory. */
private static final File CONFIG_DIR = getXDGBaseDir("XDG_CONFIG_HOME", ".config");
/** The data directory. */
private static final File DATA_DIR = getXDGBaseDir("XDG_DATA_HOME", ".local/share");
/** The cache directory. */
private static final File CACHE_DIR = getXDGBaseDir("XDG_CACHE_HOME", ".cache");
/** File for logging errors. */
public static final File LOG_FILE = new File(CONFIG_DIR, ".opsu.log");
@@ -83,6 +93,9 @@ public class Options {
/** Score database name. */
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
/** Directory where natives are unpacked. */
public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/");
/** Font file name. */
public static final String FONT_NAME = "DroidSansFallback.ttf";
@@ -119,15 +132,35 @@ public class Options {
/** Port binding. */
private static int port = 49250;
/**
* Returns whether the XDG flag in the manifest (if any) is set to "true".
* @return true if XDG directories are enabled, false otherwise
*/
private static boolean checkXDGFlag() {
JarFile jarFile = Utils.getJarFile();
if (jarFile == null)
return false;
try {
Manifest manifest = jarFile.getManifest();
if (manifest == null)
return false;
Attributes attributes = manifest.getMainAttributes();
String value = attributes.getValue("Use-XDG");
return (value != null && value.equalsIgnoreCase("true"));
} catch (IOException e) {
return false;
}
}
/**
* Returns the directory based on the XDG base directory specification for
* Unix-like operating systems, only if the system property "XDG" has been defined.
* Unix-like operating systems, only if the "XDG" flag is enabled.
* @param env the environment variable to check (XDG_*_*)
* @param fallback the fallback directory relative to ~home
* @return the XDG base directory, or the working directory if unavailable
*/
private static File getXDGBaseDir(String env, String fallback) {
if (System.getProperty("XDG") == null)
if (!USE_XDG)
return new File("./");
String OS = System.getProperty("os.name").toLowerCase();
@@ -140,8 +173,8 @@ public class Options {
rootPath = String.format("%s/%s", home, fallback);
}
File dir = new File(rootPath, "opsu");
if (!dir.isDirectory())
dir.mkdir();
if (!dir.isDirectory() && !dir.mkdir())
ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
return dir;
} else
return new File("./");
@@ -212,7 +245,7 @@ public class Options {
@Override
public void read(String s) {
int i = Integer.parseInt(s);
if (i > 0 && i < 65535)
if (i > 0 && i <= 65535)
port = i;
}
},
@@ -287,9 +320,9 @@ public class Options {
super.click(container);
if (bool) {
try {
Utils.FONT_LARGE.loadGlyphs();
Utils.FONT_MEDIUM.loadGlyphs();
Utils.FONT_DEFAULT.loadGlyphs();
Fonts.LARGE.loadGlyphs();
Fonts.MEDIUM.loadGlyphs();
Fonts.DEFAULT.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
@@ -357,7 +390,7 @@ public class Options {
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)),
(System.getProperty("os.name").toLowerCase().contains("linux"))),
KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
@Override
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
@@ -380,6 +413,7 @@ public class Options {
},
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),
DISABLE_CURSOR ("Disable Cursor", "DisableCursor", "Hide the cursor sprite.", 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),
@@ -453,16 +487,19 @@ public class Options {
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);
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true),
REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false),
DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false),
ENABLE_WATCH_SERVICE ("Enable Watch Service", "WatchService", "Watch the beatmap directory for changes. Requires a restart.", false);
/** Option name. */
private String name;
private final String name;
/** Option name, as displayed in the configuration file. */
private String displayName;
private final String displayName;
/** Option description. */
private String description;
private final String description;
/** The boolean value for the option (if applicable). */
protected boolean bool;
@@ -484,7 +521,7 @@ public class Options {
* @param displayName the option name, as displayed in the configuration file
*/
GameOption(String displayName) {
this.displayName = displayName;
this(null, displayName, null);
}
/**
@@ -604,7 +641,7 @@ public class Options {
*/
public void drag(GameContainer container, int d) {
if (type == OptionType.NUMERIC)
val = Utils.getBoundedValue(val, d, min, max);
val = Utils.clamp(val + d, min, max);
}
/**
@@ -958,6 +995,24 @@ public class Options {
*/
public static boolean isThemeSongEnabled() { return GameOption.ENABLE_THEME_SONG.getBooleanValue(); }
/**
* Returns whether or not replay seeking is enabled.
* @return true if enabled
*/
public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); }
/**
* Returns whether or not automatic checking for updates is disabled.
* @return true if disabled
*/
public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); }
/**
* Returns whether or not the beatmap watch service is enabled.
* @return true if enabled
*/
public static boolean isWatchServiceEnabled() { return GameOption.ENABLE_WATCH_SERVICE.getBooleanValue(); }
/**
* Sets the track checkpoint time, if within bounds.
* @param time the track position (in ms)
@@ -1005,6 +1060,12 @@ public class Options {
"Mouse buttons are disabled." : "Mouse buttons are enabled.");
}
/**
* Returns whether or not the cursor sprite should be hidden.
* @return true if disabled
*/
public static boolean isCursorDisabled() { return GameOption.DISABLE_CURSOR.getBooleanValue(); }
/**
* Returns the left game key.
* @return the left key code
@@ -1080,7 +1141,10 @@ public class Options {
if (beatmapDir.isDirectory())
return beatmapDir;
}
beatmapDir.mkdir(); // none found, create new directory
// none found, create new directory
if (!beatmapDir.mkdir())
ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false);
return beatmapDir;
}
@@ -1094,7 +1158,8 @@ public class Options {
return oszDir;
oszDir = new File(DATA_DIR, "SongPacks/");
oszDir.mkdir();
if (!oszDir.isDirectory() && !oszDir.mkdir())
ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false);
return oszDir;
}
@@ -1108,7 +1173,8 @@ public class Options {
return replayImportDir;
replayImportDir = new File(DATA_DIR, "ReplayImport/");
replayImportDir.mkdir();
if (!replayImportDir.isDirectory() && !replayImportDir.mkdir())
ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false);
return replayImportDir;
}
@@ -1153,7 +1219,10 @@ public class Options {
if (skinRootDir.isDirectory())
return skinRootDir;
}
skinRootDir.mkdir(); // none found, create new directory
// none found, create new directory
if (!skinRootDir.mkdir())
ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false);
return skinRootDir;
}

View File

@@ -20,7 +20,10 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.states.SongMenu;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -84,11 +87,6 @@ public class ScoreData implements Comparable<ScoreData> {
/** Drawing values. */
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset, buttonAreaHeight;
/** Button background colors. */
private static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param containerWidth the container width
@@ -99,7 +97,7 @@ public class ScoreData implements Comparable<ScoreData> {
baseY = topY;
buttonWidth = containerWidth * 0.4f;
float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f);
buttonHeight = Math.max(gradeHeight, Fonts.DEFAULT.getLineHeight() * 3.03f);
buttonOffset = buttonHeight + gradeHeight / 10f;
buttonAreaHeight = (SongMenu.MAX_SCORE_BUTTONS - 1) * buttonOffset + buttonHeight;
}
@@ -155,14 +153,14 @@ public class ScoreData implements Comparable<ScoreData> {
}
/**
* Empty constructor.
* Creates an empty score data object.
*/
public ScoreData() {}
/**
* Constructor.
* @param rs the ResultSet to read from (at the current cursor position)
* @throws SQLException
* Builds a score data object from a database result set.
* @param rs the {@link ResultSet} to read from (at the current cursor position)
* @throws SQLException if a database access error occurs or the result set is closed
*/
public ScoreData(ResultSet rs) throws SQLException {
this.timestamp = rs.getLong(1);
@@ -244,46 +242,52 @@ public class ScoreData implements Comparable<ScoreData> {
* @param rank the score rank
* @param prevScore the previous (lower) score, or -1 if none
* @param focus whether the button is focused
* @param t the animation progress [0,1]
*/
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus) {
Image img = getGrade().getMenuImage();
float textX = baseX + buttonWidth * 0.24f;
float edgeX = baseX + buttonWidth * 0.98f;
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus, float t) {
float x = baseX - buttonWidth * (1 - AnimationEquation.OUT_BACK.calc(t)) / 2.5f;
float textX = x + buttonWidth * 0.24f;
float edgeX = x + buttonWidth * 0.98f;
float y = baseY + position;
float midY = y + buttonHeight / 2f;
float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f;
float marginY = Fonts.DEFAULT.getLineHeight() * 0.01f;
Color c = Colors.WHITE_FADE;
float alpha = t;
float oldAlpha = c.a;
c.a = alpha;
// rectangle outline
g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
g.fillRect(baseX, y, buttonWidth, buttonHeight);
Color rectColor = (focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL;
float oldRectAlpha = rectColor.a;
rectColor.a *= AnimationEquation.IN_QUAD.calc(alpha);
g.setColor(rectColor);
g.fillRect(x, y, buttonWidth, buttonHeight);
rectColor.a = oldRectAlpha;
// rank
if (focus) {
Utils.FONT_LARGE.drawString(
baseX + buttonWidth * 0.04f,
y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
Integer.toString(rank + 1), Color.white
Fonts.LARGE.drawString(
x + buttonWidth * 0.04f,
y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2f,
Integer.toString(rank + 1), c
);
}
// grade image
img.drawCentered(baseX + buttonWidth * 0.15f, midY);
Image img = getGrade().getMenuImage();
img.setAlpha(alpha);
img.drawCentered(x + buttonWidth * 0.15f, midY);
img.setAlpha(1f);
// score
float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
Utils.FONT_MEDIUM.drawString(
textX, y + textOffset,
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
Color.white
);
float textOffset = (buttonHeight - Fonts.MEDIUM.getLineHeight() - Fonts.SMALL.getLineHeight()) / 2f;
Fonts.MEDIUM.drawString(textX, y + textOffset,
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo), c);
// hit counts (custom: osu! shows user instead, above score)
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
Utils.FONT_SMALL.drawString(
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player),
Color.white
);
Fonts.SMALL.drawString(textX, y + textOffset + Fonts.MEDIUM.getLineHeight(),
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player), c);
// mods
StringBuilder sb = new StringBuilder();
@@ -296,39 +300,30 @@ public class ScoreData implements Comparable<ScoreData> {
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
String modString = sb.toString();
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(modString),
y + marginY, modString, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(modString), y + marginY, modString, c);
}
// accuracy
String accuracy = String.format("%.2f%%", getScorePercent());
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
accuracy, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(accuracy), y + marginY + Fonts.DEFAULT.getLineHeight(), accuracy, c);
// score difference
String diff = (prevScore < 0 || score < prevScore) ?
"-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(diff),
y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
diff, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(diff), y + marginY + Fonts.DEFAULT.getLineHeight() * 2, diff, c);
// time since
if (getTimeSince() != null) {
Image clock = GameImage.HISTORY.getImage();
clock.drawCentered(baseX + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
Utils.FONT_DEFAULT.drawString(
baseX + buttonWidth * 1.03f + clock.getWidth(),
midY - Utils.FONT_DEFAULT.getLineHeight() / 2f,
getTimeSince(), Color.white
clock.drawCentered(x + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
Fonts.DEFAULT.drawString(
x + buttonWidth * 1.03f + clock.getWidth(),
midY - Fonts.DEFAULT.getLineHeight() / 2f,
getTimeSince(), c
);
}
c.a = oldAlpha;
}
/**

View File

@@ -24,14 +24,15 @@ import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -43,13 +44,10 @@ import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.jar.JarFile;
import javax.imageio.ImageIO;
@@ -60,16 +58,10 @@ import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.font.effects.Effect;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
import com.sun.jna.platform.FileUtils;
@@ -77,34 +69,6 @@ import com.sun.jna.platform.FileUtils;
* Contains miscellaneous utilities.
*/
public class Utils {
/** Game colors. */
public static final Color
COLOR_BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
COLOR_WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
COLOR_BLUE_DIVIDER = new Color(49, 94, 237),
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
COLOR_BLUE_BUTTON = new Color(40, 129, 237),
COLOR_ORANGE_BUTTON = new Color(200, 90, 3),
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
COLOR_RED_HOVER = new Color(255, 112, 112),
COLOR_GREEN = new Color(137, 201, 79),
COLOR_LIGHT_ORANGE = new Color(255,192,128),
COLOR_LIGHT_GREEN = new Color(128,255,128),
COLOR_LIGHT_BLUE = new Color(128,128,255),
COLOR_GREEN_SEARCH = new Color(173, 255, 47),
COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
COLOR_RED_HIGHLIGHT = new Color(246, 154, 161),
COLOR_BLUE_HIGHLIGHT = new Color(173, 216, 230);
/** Game fonts. */
public static UnicodeFont
FONT_DEFAULT, FONT_BOLD,
FONT_XLARGE, FONT_LARGE, FONT_MEDIUM, FONT_SMALL;
/** Set of all Unicode strings already loaded per font. */
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
/**
* List of illegal filename characters.
* @see #cleanFileName(String, char)
@@ -115,7 +79,7 @@ public class Utils {
24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47
};
static {
Arrays.sort(illegalChars);
Arrays.sort(illegalChars);
}
// game-related variables
@@ -128,10 +92,8 @@ public class Utils {
* Initializes game settings and class data.
* @param container the game container
* @param game the game object
* @throws SlickException
*/
public static void init(GameContainer container, StateBasedGame game)
throws SlickException {
public static void init(GameContainer container, StateBasedGame game) {
input = container.getInput();
int width = container.getWidth();
int height = container.getHeight();
@@ -149,23 +111,8 @@ public class Utils {
GameImage.init(width, height);
// create fonts
float fontBase = 12f * GameImage.getUIscale();
try {
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
FONT_DEFAULT = new UnicodeFont(font);
FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
FONT_LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(FONT_DEFAULT, colorEffect);
loadFont(FONT_BOLD, colorEffect);
loadFont(FONT_XLARGE, colorEffect);
loadFont(FONT_LARGE, colorEffect);
loadFont(FONT_MEDIUM, colorEffect);
loadFont(FONT_SMALL, colorEffect);
Fonts.init();
} catch (Exception e) {
ErrorHandler.error("Failed to load fonts.", e, true);
}
@@ -206,36 +153,18 @@ public class Utils {
}
/**
* Returns a bounded value for a base value and displacement.
* @param base the initial value
* @param diff the value change
* @param min the minimum value
* @param max the maximum value
* @return the bounded value
* Clamps a value between a lower and upper bound.
* @param val the value to clamp
* @param low the lower bound
* @param high the upper bound
* @return the clamped value
* @author fluddokt
*/
public static int getBoundedValue(int base, int diff, int min, int max) {
int val = base + diff;
if (val < min)
val = min;
else if (val > max)
val = max;
return val;
}
/**
* Returns a bounded value for a base value and displacement.
* @param base the initial value
* @param diff the value change
* @param min the minimum value
* @param max the maximum value
* @return the bounded value
*/
public static float getBoundedValue(float base, float diff, float min, float max) {
float val = base + diff;
if (val < min)
val = min;
else if (val > max)
val = max;
public static int clamp(int val, int low, int high) {
if (val < low)
return low;
if (val > high)
return high;
return val;
}
@@ -269,6 +198,13 @@ public class Utils {
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
}
/**
* Linear interpolation of a and b at t.
*/
public static float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
/**
* Returns true if a game input key is pressed (mouse/keyboard left/right).
* @return true if pressed
@@ -289,11 +225,9 @@ public class Utils {
public static void takeScreenShot() {
// create the screenshot directory
File dir = Options.getScreenshotDir();
if (!dir.isDirectory()) {
if (!dir.mkdir()) {
ErrorHandler.error("Failed to create screenshot directory.", null, false);
return;
}
if (!dir.isDirectory() && !dir.mkdir()) {
ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false);
return;
}
// create file name
@@ -333,54 +267,6 @@ public class Utils {
}.start();
}
/**
* Loads a Unicode font.
* @param font the font to load
* @param effect the font effect
* @throws SlickException
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for a beatmap's Unicode title and artist strings.
* @param font the font to add the glyphs to
* @param title the title string
* @param artist the artist string
*/
public static void loadGlyphs(UnicodeFont font, String title, String artist) {
// get set of added strings
HashSet<String> set = loadedGlyphs.get(font);
if (set == null) {
set = new HashSet<String>();
loadedGlyphs.put(font, set);
}
// add glyphs if not in set
boolean glyphsAdded = false;
if (title != null && !title.isEmpty() && !set.contains(title)) {
font.addGlyphs(title);
set.add(title);
glyphsAdded = true;
}
if (artist != null && !artist.isEmpty() && !set.contains(artist)) {
font.addGlyphs(artist);
set.add(artist);
glyphsAdded = true;
}
if (glyphsAdded) {
try {
font.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
}
}
/**
* Returns a human-readable representation of a given number of bytes.
* @param bytes the number of bytes
@@ -407,15 +293,15 @@ public class Utils {
return null;
boolean doReplace = (replace > 0 && Arrays.binarySearch(illegalChars, replace) < 0);
StringBuilder cleanName = new StringBuilder();
for (int i = 0, n = badFileName.length(); i < n; i++) {
int c = badFileName.charAt(i);
if (Arrays.binarySearch(illegalChars, c) < 0)
cleanName.append((char) c);
else if (doReplace)
cleanName.append(replace);
}
return cleanName.toString();
StringBuilder cleanName = new StringBuilder();
for (int i = 0, n = badFileName.length(); i < n; i++) {
int c = badFileName.charAt(i);
if (Arrays.binarySearch(illegalChars, c) < 0)
cleanName.append((char) c);
else if (doReplace)
cleanName.append(replace);
}
return cleanName.toString();
}
/**
@@ -474,50 +360,12 @@ public class Utils {
dir.delete();
}
/**
* Wraps the given string into a list of split lines based on the width.
* @param text the text to split
* @param font the font used to draw the string
* @param width the maximum width of a line
* @return the list of split strings
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
*/
public static List<String> wrap(String text, org.newdawn.slick.Font font, int width) {
List<String> list = new ArrayList<String>();
String str = text;
String line = "";
int i = 0;
int lastSpace = -1;
while (i < str.length()) {
char c = str.charAt(i);
if (Character.isWhitespace(c))
lastSpace = i;
String append = line + c;
if (font.getWidth(append) > width) {
int split = (lastSpace != -1) ? lastSpace : i;
int splitTrimmed = split;
if (lastSpace != -1 && split < str.length() - 1)
splitTrimmed++;
list.add(str.substring(0, split));
str = str.substring(splitTrimmed);
line = "";
i = 0;
lastSpace = -1;
} else {
line = append;
i++;
}
}
if (str.length() != 0)
list.add(str);
return list;
}
/**
* Returns a the contents of a URL as a string.
* @param url the remote URL
* @return the contents as a string, or null if any error occurred
* @author Roland Illig (http://stackoverflow.com/a/4308662)
* @throws IOException if an I/O exception occurs
*/
public static String readDataFromUrl(URL url) throws IOException {
// open connection
@@ -553,6 +401,7 @@ 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
* @throws IOException if an I/O exception occurs
*/
public static JSONObject readJsonObjectFromUrl(URL url) throws IOException {
String s = Utils.readDataFromUrl(url);
@@ -571,6 +420,7 @@ public class Utils {
* Returns a JSON array from a URL.
* @param url the remote URL
* @return the JSON array, or null if an error occurred
* @throws IOException if an I/O exception occurs
*/
public static JSONArray readJsonArrayFromUrl(URL url) throws IOException {
String s = Utils.readDataFromUrl(url);
@@ -640,32 +490,6 @@ public class Utils {
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
@@ -674,8 +498,25 @@ public class Utils {
return Opsu.class.getResource(String.format("%s.class", Opsu.class.getSimpleName())).toString().startsWith("jar:");
}
/**
* Returns the JarFile for the application.
* @return the JAR file, or null if it could not be determined
*/
public static JarFile getJarFile() {
if (!isJarRunning())
return null;
try {
return new JarFile(new File(Opsu.class.getProtectionDomain().getCodeSource().getLocation().toURI()), false);
} catch (URISyntaxException | IOException e) {
Log.error("Could not determine the JAR file.", e);
return null;
}
}
/**
* Returns the directory where the application is being run.
* @return the directory, or null if it could not be determined
*/
public static File getRunningDirectory() {
try {
@@ -695,4 +536,29 @@ public class Utils {
public static boolean parseBoolean(String s) {
return (Integer.parseInt(s) == 1);
}
/**
* Returns the git hash of the remote-tracking branch 'origin/master' from the
* most recent update to the working directory (e.g. fetch or successful push).
* @return the 40-character SHA-1 hash, or null if it could not be determined
*/
public static String getGitHash() {
if (isJarRunning())
return null;
File f = new File(".git/refs/remotes/origin/master");
if (!f.isFile())
return null;
try (BufferedReader in = new BufferedReader(new FileReader(f))) {
char[] sha = new char[40];
if (in.read(sha, 0, sha.length) < sha.length)
return null;
for (int i = 0; i < sha.length; i++) {
if (Character.digit(sha[i], 16) == -1)
return null;
}
return String.valueOf(sha);
} catch (IOException e) {
return null;
}
}
}

View File

@@ -40,10 +40,10 @@ public enum HitSound implements SoundController.SoundComponent {
// TAIKO ("taiko", 4);
/** The sample set name. */
private String name;
private final String name;
/** The sample set index. */
private int index;
private final int index;
/** Total number of sample sets. */
public static final int SIZE = values().length;
@@ -77,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent {
private static SampleSet currentDefaultSampleSet = SampleSet.NORMAL;
/** The file name. */
private String filename;
private final String filename;
/** The Clip associated with the hit sound. */
private HashMap<SampleSet, MultiClip> clips;

View File

@@ -49,12 +49,15 @@ public class MultiClip {
private byte[] audioData;
/** The name given to this clip. */
private String name;
private final String name;
/**
* Constructor.
* @param name the clip name
* @param audioIn the associated AudioInputStream
* @throws IOException if an input or output error occurs
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
this.name = name;
@@ -105,6 +108,8 @@ public class MultiClip {
* Plays the clip with the specified volume.
* @param volume the volume the play at
* @param listener the line listener
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
public void start(float volume, LineListener listener) throws LineUnavailableException {
Clip clip = getClip();
@@ -130,6 +135,8 @@ public class MultiClip {
* If no clip is available, then a new one is created if under MAX_CLIPS.
* Otherwise, an existing clip will be returned.
* @return the Clip to play
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
private Clip getClip() throws LineUnavailableException {
// TODO:

View File

@@ -22,6 +22,7 @@ import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.io.IOException;
@@ -41,6 +42,7 @@ import org.newdawn.slick.MusicListener;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.openal.Audio;
import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.util.ResourceLoader;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/**
@@ -87,6 +89,13 @@ public class MusicController {
public static void play(final Beatmap beatmap, final boolean loop, final boolean preview) {
// new track: load and play
if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
final File audioFile = beatmap.audioFilename;
if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) {
UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName()));
System.out.println(beatmap);
return;
}
reset();
System.gc();
@@ -96,7 +105,7 @@ public class MusicController {
trackLoader = new Thread() {
@Override
public void run() {
loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
loadTrack(audioFile, (preview) ? beatmap.previewTime : 0, loop);
}
};
trackLoader.start();
@@ -221,6 +230,7 @@ public class MusicController {
/**
* Fades out the track.
* @param duration the fade time (in ms)
*/
public static void fadeOut(int duration) {
if (isPlaying())

View File

@@ -59,6 +59,9 @@ public class SoundController {
/** Sample volume multiplier, from timing points [0, 1]. */
private static float sampleVolumeMultiplier = 1f;
/** Whether all sounds are muted. */
private static boolean isMuted;
/** The name of the current sound file being loaded. */
private static String currentFileName;
@@ -261,7 +264,7 @@ public class SoundController {
if (clip == null) // clip failed to load properly
return;
if (volume > 0f) {
if (volume > 0f && !isMuted) {
try {
clip.start(volume, listener);
} catch (LineUnavailableException e) {
@@ -317,6 +320,12 @@ public class SoundController {
playClip(s.getClip(), Options.getHitSoundVolume() * sampleVolumeMultiplier * Options.getMasterVolume(), null);
}
/**
* Mutes or unmutes all sounds (hit sounds and sound effects).
* @param mute true to mute, false to unmute
*/
public static void mute(boolean mute) { isMuted = mute; }
/**
* Returns the name of the current file being loaded, or null if none.
*/

View File

@@ -42,7 +42,7 @@ public enum SoundEffect implements SoundController.SoundComponent {
SPINNERSPIN ("spinnerspin");
/** The file name. */
private String filename;
private final String filename;
/** The Clip associated with the sound effect. */
private MultiClip clip;

View File

@@ -23,6 +23,7 @@ import itdelatrisu.opsu.Options;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
@@ -36,16 +37,37 @@ public class Beatmap implements Comparable<Beatmap> {
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();
@SuppressWarnings("serial")
private static final LRUCache<File, ImageLoader> bgImageCache = new LRUCache<File, ImageLoader>(10) {
@Override
public void eldestRemoved(Map.Entry<File, ImageLoader> eldest) {
if (eldest.getKey() == lastBG)
lastBG = null;
ImageLoader imageLoader = eldest.getValue();
imageLoader.destroy();
}
};
/** The last background image loaded. */
private static File lastBG;
/**
* Returns the background image cache.
* Clears the background image 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 static BeatmapImageCache getBackgroundImageCache() { return bgImageCache; }
public static void clearBackgroundImageCache() { bgImageCache.clear(); }
/** The OSU File object associated with this beatmap. */
private File file;
/** MD5 hash of this file. */
public String md5Hash;
/** The star rating. */
public double starRating = -1;
/**
* [General]
*/
@@ -138,7 +160,7 @@ public class Beatmap implements Comparable<Beatmap> {
public float HPDrainRate = 5f;
/** CS: Size of circles and sliders (0:large ~ 10:small). */
public float circleSize = 4f;
public float circleSize = 5f;
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
public float overallDifficulty = 5f;
@@ -147,7 +169,7 @@ public class Beatmap implements Comparable<Beatmap> {
public float approachRate = -1f;
/** Slider movement speed multiplier. */
public float sliderMultiplier = 1f;
public float sliderMultiplier = 1.4f;
/** Rate at which slider ticks are placed (x per beat). */
public float sliderTickRate = 1f;
@@ -185,9 +207,6 @@ public class Beatmap implements Comparable<Beatmap> {
/** Slider border color. If null, the skin value is used. */
public Color sliderBorder;
/** MD5 hash of this file. */
public String md5Hash;
/**
* [HitObjects]
*/
@@ -255,47 +274,74 @@ public class Beatmap implements Comparable<Beatmap> {
}
/**
* Draws the beatmap background.
* Loads the beatmap background image.
*/
public void loadBackground() {
if (bg == null || bgImageCache.containsKey(bg) || !bg.isFile())
return;
if (lastBG != null) {
ImageLoader lastImageLoader = bgImageCache.get(lastBG);
if (lastImageLoader != null && lastImageLoader.isLoading()) {
lastImageLoader.interrupt(); // only allow loading one image at a time
bgImageCache.remove(lastBG);
}
}
ImageLoader imageLoader = new ImageLoader(bg);
bgImageCache.put(bg, imageLoader);
imageLoader.load(true);
lastBG = bg;
}
/**
* Returns whether the beatmap background image is currently loading.
* @return true if loading
*/
public boolean isBackgroundLoading() {
if (bg == null)
return false;
ImageLoader imageLoader = bgImageCache.get(bg);
return (imageLoader != null && imageLoader.isLoading());
}
/**
* Draws the beatmap background image.
* @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) {
public boolean drawBackground(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
ImageLoader imageLoader = bgImageCache.get(bg);
if (imageLoader == null)
return false;
Image bgImage = imageLoader.getImage();
if (bgImage == null)
return true;
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);
return true;
}

View File

@@ -0,0 +1,544 @@
/*
* 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.db.BeatmapDB;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.newdawn.slick.util.Log;
/**
* osu!tp's beatmap difficulty algorithm.
*
* @author Tom94 (https://github.com/Tom94/AiModtpDifficultyCalculator)
*/
public class BeatmapDifficultyCalculator {
/** Difficulty types. */
public static final int DIFFICULTY_SPEED = 0, DIFFICULTY_AIM = 1;
/** The star scaling factor. */
private static final double STAR_SCALING_FACTOR = 0.045;
/** The scaling factor that favors extremes. */
private static final double EXTREME_SCALING_FACTOR = 0.5;
/** The playfield width. */
private static final float PLAYFIELD_WIDTH = 512f;
/**
* In milliseconds. For difficulty calculation we will only look at the highest strain value in each
* time interval of size STRAIN_STEP.This is to eliminate higher influence of stream over aim by simply
* having more HitObjects with high strain. The higher this value, the less strains there will be,
* indirectly giving long beatmaps an advantage.
*/
private static final double STRAIN_STEP = 400;
/** The weighting of each strain value decays to 0.9 * its previous value. */
private static final double DECAY_WEIGHT = 0.9;
/** The beatmap. */
private final Beatmap beatmap;
/** The beatmap's hit objects. */
private tpHitObject[] tpHitObjects;
/** The computed star rating. */
private double starRating = -1;
/** The computed difficulties, indexed by the {@code DIFFICULTY_*} constants. */
private double[] difficulties = { -1, -1 };
/** The computed stars, indexed by the {@code DIFFICULTY_*} constants. */
private double[] stars = { -1, -1 };
/**
* Constructor. Call {@link #calculate()} to run all computations.
* <p>
* If any parts of the beatmap have not yet been loaded (e.g. timing points,
* hit objects), they will be loaded here.
* @param beatmap the beatmap
*/
public BeatmapDifficultyCalculator(Beatmap beatmap) {
this.beatmap = beatmap;
if (beatmap.timingPoints == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap);
}
/**
* Returns the beatmap's star rating.
*/
public double getStarRating() { return starRating; }
/**
* Returns the difficulty value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getDifficulty(int type) { return difficulties[type]; }
/**
* Returns the star value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getStars(int type) { return stars[type]; }
/**
* Calculates the difficulty values and star ratings for the beatmap.
*/
public void calculate() {
if (beatmap.objects == null || beatmap.timingPoints == null) {
Log.error(String.format("Trying to calculate difficulty values for beatmap '%s' with %s not yet loaded.",
beatmap.toString(), (beatmap.objects == null) ? "hit objects" : "timing points"));
return;
}
// Fill our custom tpHitObject class, that carries additional information
// TODO: apply hit object stacking algorithm?
HitObject[] hitObjects = beatmap.objects;
this.tpHitObjects = new tpHitObject[hitObjects.length];
float circleRadius = (PLAYFIELD_WIDTH / 16.0f) * (1.0f - 0.7f * (beatmap.circleSize - 5.0f) / 5.0f);
int timingPointIndex = 0;
float beatLengthBase = 1, beatLength = 1;
if (!beatmap.timingPoints.isEmpty()) {
TimingPoint timingPoint = beatmap.timingPoints.get(0);
if (!timingPoint.isInherited()) {
beatLengthBase = beatLength = timingPoint.getBeatLength();
timingPointIndex++;
}
}
for (int i = 0; i < hitObjects.length; i++) {
HitObject hitObject = hitObjects[i];
// pass beatLength to hit objects
int hitObjectTime = hitObject.getTime();
while (timingPointIndex < beatmap.timingPoints.size()) {
TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
if (timingPoint.getTime() > hitObjectTime)
break;
if (!timingPoint.isInherited())
beatLengthBase = beatLength = timingPoint.getBeatLength();
else
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
timingPointIndex++;
}
tpHitObjects[i] = new tpHitObject(hitObject, circleRadius, beatmap, beatLength);
}
if (!calculateStrainValues()) {
Log.error("Could not compute strain values. Aborting difficulty calculation.");
return;
}
// OverallDifficulty is not considered in this algorithm and neither is HpDrainRate.
// That means, that in this form the algorithm determines how hard it physically is
// to play the map, assuming, that too much of an error will not lead to a death.
// It might be desirable to include OverallDifficulty into map difficulty, but in my
// personal opinion it belongs more to the weighting of the actual performance
// and is superfluous in the beatmap difficulty rating.
// If it were to be considered, then I would look at the hit window of normal HitCircles only,
// since Sliders and Spinners are (almost) "free" 300s and take map length into account as well.
difficulties[DIFFICULTY_SPEED] = calculateDifficulty(DIFFICULTY_SPEED);
difficulties[DIFFICULTY_AIM] = calculateDifficulty(DIFFICULTY_AIM);
// The difficulty can be scaled by any desired metric.
// In osu!tp it gets squared to account for the rapid increase in difficulty as the
// limit of a human is approached. (Of course it also gets scaled afterwards.)
// It would not be suitable for a star rating, therefore:
// The following is a proposal to forge a star rating from 0 to 5. It consists of taking
// the square root of the difficulty, since by simply scaling the easier
// 5-star maps would end up with one star.
stars[DIFFICULTY_SPEED] = Math.sqrt(difficulties[DIFFICULTY_SPEED]) * STAR_SCALING_FACTOR;
stars[DIFFICULTY_AIM] = Math.sqrt(difficulties[DIFFICULTY_AIM]) * STAR_SCALING_FACTOR;
// Again, from own observations and from the general opinion of the community
// a map with high speed and low aim (or vice versa) difficulty is harder,
// than a map with mediocre difficulty in both. Therefore we can not just add
// both difficulties together, but will introduce a scaling that favors extremes.
// Another approach to this would be taking Speed and Aim separately to a chosen
// power, which again would be equivalent. This would be more convenient if
// the hit window size is to be considered as well.
// Note: The star rating is tuned extremely tight! Airman (/b/104229) and
// Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars.
// Expect the easier kind of maps that officially get 5 stars to obtain around 2 by
// this metric. The tutorial still scores about half a star.
// Tune by yourself as you please. ;)
this.starRating = stars[DIFFICULTY_SPEED] + stars[DIFFICULTY_AIM] +
Math.abs(stars[DIFFICULTY_SPEED] - stars[DIFFICULTY_AIM]) * EXTREME_SCALING_FACTOR;
}
/**
* Computes the strain values for the beatmap.
* @return true if successful, false otherwise
*/
private boolean calculateStrainValues() {
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from
// the strain value of CurrentHitObject and environment.
if (tpHitObjects.length == 0) {
Log.warn("Can not compute difficulty of empty beatmap.");
return false;
}
tpHitObject currentHitObject = tpHitObjects[0];
tpHitObject nextHitObject;
int index = 0;
// First hitObject starts at strain 1. 1 is the default for strain values,
// so we don't need to set it here. See tpHitObject.
while (++index < tpHitObjects.length) {
nextHitObject = tpHitObjects[index];
nextHitObject.calculateStrains(currentHitObject);
currentHitObject = nextHitObject;
}
return true;
}
/**
* Calculates the difficulty value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
* @return the difficulty value
*/
private double calculateDifficulty(int type) {
// Find the highest strain value within each strain step
List<Double> highestStrains = new ArrayList<Double>();
double intervalEndTime = STRAIN_STEP;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
tpHitObject previousHitObject = null;
for (int i = 0; i < tpHitObjects.length; i++) {
tpHitObject hitObject = tpHitObjects[i];
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.baseHitObject.getTime() > intervalEndTime) {
highestStrains.add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last
// hitObject we encountered, take its strain and apply the decay until the beginning of the next interval.
if (previousHitObject == null)
maximumStrain = 0;
else {
double decay = Math.pow(tpHitObject.DECAY_BASE[type], (intervalEndTime - previousHitObject.baseHitObject.getTime()) / 1000);
maximumStrain = previousHitObject.getStrain(type) * decay;
}
// Go to the next time interval
intervalEndTime += STRAIN_STEP;
}
// Obtain maximum strain
if (hitObject.getStrain(type) > maximumStrain)
maximumStrain = hitObject.getStrain(type);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
Collections.sort(highestStrains, Collections.reverseOrder()); // Sort from highest to lowest strain.
for (double strain : highestStrains) {
difficulty += weight * strain;
weight *= DECAY_WEIGHT;
}
return difficulty;
}
}
/**
* Hit object helper class for calculating strains.
*/
class tpHitObject {
/**
* Factor by how much speed / aim strain decays per second. Those values are results
* of tweaking a lot and taking into account general feedback.
* Opinionated observation: Speed is easier to maintain than accurate jumps.
*/
public static final double[] DECAY_BASE = { 0.3, 0.15 };
/** Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming. */
private static final double ALMOST_DIAMETER = 90;
/**
* Pseudo threshold values to distinguish between "singles" and "streams".
* Of course the border can not be defined clearly, therefore the algorithm
* has a smooth transition between those values. They also are based on tweaking
* and general feedback.
*/
private static final double STREAM_SPACING_TRESHOLD = 110, SINGLE_SPACING_TRESHOLD = 125;
/**
* Scaling values for weightings to keep aim and speed difficulty in balance.
* Found from testing a very large map pool (containing all ranked maps) and
* keeping the average values the same.
*/
private static final double[] SPACING_WEIGHT_SCALING = { 1400, 26.25 };
/**
* In milliseconds. The smaller the value, the more accurate sliders are approximated.
* 0 leads to an infinite loop, so use something bigger.
*/
private static final int LAZY_SLIDER_STEP_LENGTH = 1;
/** The base hit object. */
public final HitObject baseHitObject;
/** The strain values, indexed by the {@code DIFFICULTY_*} constants. */
private double[] strains = { 1, 1 };
/** The normalized start and end positions. */
private Vec2f normalizedStartPosition, normalizedEndPosition;
/** The slider lengths. */
private float lazySliderLengthFirst = 0, lazySliderLengthSubsequent = 0;
/**
* Constructor.
* @param baseHitObject the base hit object
* @param circleRadius the circle radius
* @param beatmap the beatmap that contains the hit object
* @param beatLength the current beat length
*/
public tpHitObject(HitObject baseHitObject, float circleRadius, Beatmap beatmap, float beatLength) {
this.baseHitObject = baseHitObject;
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = (52.0f / circleRadius);
normalizedStartPosition = new Vec2f(baseHitObject.getX(), baseHitObject.getY()).scale(scalingFactor);
// Calculate approximation of lazy movement on the slider
if (baseHitObject.isSlider()) {
tpSlider slider = new tpSlider(baseHitObject, beatmap.sliderMultiplier, beatLength);
// Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests.
float sliderFollowCircleRadius = circleRadius * 3;
int segmentLength = slider.getSegmentLength(); // baseHitObject.Length / baseHitObject.SegmentCount;
int segmentEndTime = baseHitObject.getTime() + segmentLength;
// For simplifying this step we use actual osu! coordinates and simply scale the length,
// that we obtain by the ScalingFactor later
Vec2f cursorPos = new Vec2f(baseHitObject.getX(), baseHitObject.getY());
// Actual computation of the first lazy curve
for (int time = baseHitObject.getTime() + LAZY_SLIDER_STEP_LENGTH; time < segmentEndTime; time += LAZY_SLIDER_STEP_LENGTH) {
Vec2f difference = slider.getPositionAtTime(time).sub(cursorPos);
float distance = difference.len();
// Did we move away too far?
if (distance > sliderFollowCircleRadius) {
// Yep, we need to move the cursor
difference.normalize(); // Obtain the direction of difference. We do no longer need the actual difference
distance -= sliderFollowCircleRadius;
cursorPos.add(difference.cpy().scale(distance)); // We move the cursor just as far as needed to stay in the follow circle
lazySliderLengthFirst += distance;
}
}
lazySliderLengthFirst *= scalingFactor;
// If we have an odd amount of repetitions the current position will be the end of the slider.
// Note that this will -always- be triggered if baseHitObject.SegmentCount <= 1, because
// baseHitObject.SegmentCount can not be smaller than 1. Therefore normalizedEndPosition will
// always be initialized
if (baseHitObject.getRepeatCount() % 2 == 1)
normalizedEndPosition = cursorPos.cpy().scale(scalingFactor);
// If we have more than one segment, then we also need to compute the length of subsequent
// lazy curves. They are different from the first one, since the first one starts right
// at the beginning of the slider.
if (baseHitObject.getRepeatCount() > 1) {
// Use the next segment
segmentEndTime += segmentLength;
for (int time = segmentEndTime - segmentLength + LAZY_SLIDER_STEP_LENGTH; time < segmentEndTime; time += LAZY_SLIDER_STEP_LENGTH) {
Vec2f difference = slider.getPositionAtTime(time).sub(cursorPos);
float distance = difference.len();
// Did we move away too far?
if (distance > sliderFollowCircleRadius) {
// Yep, we need to move the cursor
difference.normalize(); // Obtain the direction of difference. We do no longer need the actual difference
distance -= sliderFollowCircleRadius;
cursorPos.add(difference.cpy().scale(distance)); // We move the cursor just as far as needed to stay in the follow circle
lazySliderLengthSubsequent += distance;
}
}
lazySliderLengthSubsequent *= scalingFactor;
// If we have an even amount of repetitions the current position will be the end of the slider
if (baseHitObject.getRepeatCount() % 2 == 0) // == 1)
normalizedEndPosition = cursorPos.cpy().scale(scalingFactor);
}
} else {
// We have a normal HitCircle or a spinner
normalizedEndPosition = normalizedStartPosition.cpy(); //baseHitObject.EndPosition * ScalingFactor;
}
}
/**
* Returns the strain value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getStrain(int type) { return strains[type]; }
/**
* Calculates the strain values given the previous hit object.
* @param previousHitObject the previous hit object
*/
public void calculateStrains(tpHitObject previousHitObject) {
calculateSpecificStrain(previousHitObject, BeatmapDifficultyCalculator.DIFFICULTY_SPEED);
calculateSpecificStrain(previousHitObject, BeatmapDifficultyCalculator.DIFFICULTY_AIM);
}
/**
* Returns the spacing weight for a distance.
* @param distance the distance
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
private static double spacingWeight(double distance, int type) {
// Caution: The subjective values are strong with this one
switch (type) {
case BeatmapDifficultyCalculator.DIFFICULTY_SPEED:
double weight;
if (distance > SINGLE_SPACING_TRESHOLD)
weight = 2.5;
else if (distance > STREAM_SPACING_TRESHOLD)
weight = 1.6 + 0.9 * (distance - STREAM_SPACING_TRESHOLD) / (SINGLE_SPACING_TRESHOLD - STREAM_SPACING_TRESHOLD);
else if (distance > ALMOST_DIAMETER)
weight = 1.2 + 0.4 * (distance - ALMOST_DIAMETER) / (STREAM_SPACING_TRESHOLD - ALMOST_DIAMETER);
else if (distance > ALMOST_DIAMETER / 2)
weight = 0.95 + 0.25 * (distance - (ALMOST_DIAMETER / 2)) / (ALMOST_DIAMETER / 2);
else
weight = 0.95;
return weight;
case BeatmapDifficultyCalculator.DIFFICULTY_AIM:
return Math.pow(distance, 0.99);
default:
// Should never happen.
return 0;
}
}
/**
* Calculates the strain value for a difficulty type given the previous hit object.
* @param previousHitObject the previous hit object
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
private void calculateSpecificStrain(tpHitObject previousHitObject, int type) {
double addition = 0;
double timeElapsed = baseHitObject.getTime() - previousHitObject.baseHitObject.getTime();
double decay = Math.pow(DECAY_BASE[type], timeElapsed / 1000);
if (baseHitObject.isSpinner()) {
// Do nothing for spinners
} else if (baseHitObject.isSlider()) {
switch (type) {
case BeatmapDifficultyCalculator.DIFFICULTY_SPEED:
// For speed strain we treat the whole slider as a single spacing entity,
// since "Speed" is about how hard it is to click buttons fast.
// The spacing weight exists to differentiate between being able to easily
// alternate or having to single.
addition = spacingWeight(previousHitObject.lazySliderLengthFirst +
previousHitObject.lazySliderLengthSubsequent * (Math.max(previousHitObject.baseHitObject.getRepeatCount(), 1) - 1) +
distanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[type];
break;
case BeatmapDifficultyCalculator.DIFFICULTY_AIM:
// For Aim strain we treat each slider segment and the jump after the end of
// the slider as separate jumps, since movement-wise there is no difference
// to multiple jumps.
addition = (spacingWeight(previousHitObject.lazySliderLengthFirst, type) +
spacingWeight(previousHitObject.lazySliderLengthSubsequent, type) * (Math.max(previousHitObject.baseHitObject.getRepeatCount(), 1) - 1) +
spacingWeight(distanceTo(previousHitObject), type)) * SPACING_WEIGHT_SCALING[type];
break;
}
} else if (baseHitObject.isCircle()) {
addition = spacingWeight(distanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[type];
}
// Scale addition by the time, that elapsed. Filter out HitObjects that are too
// close to be played anyway to avoid crazy values by division through close to zero.
// You will never find maps that require this amongst ranked maps.
addition /= Math.max(timeElapsed, 50);
strains[type] = previousHitObject.strains[type] * decay + addition;
}
/**
* Returns the distance to another hit object.
* @param other the other hit object
*/
public double distanceTo(tpHitObject other) {
// Scale the distance by circle size.
return (normalizedStartPosition.cpy().sub(other.normalizedEndPosition)).len();
}
}
/**
* Slider helper class to fill in some missing pieces needed in the strain calculations.
*/
class tpSlider {
/** The slider start time. */
private final int startTime;
/** The time duration of the slider, in milliseconds. */
private final int sliderTime;
/** The slider Curve. */
private final Curve curve;
/**
* Constructor.
* @param hitObject the hit object
* @param sliderMultiplier the slider movement speed multiplier
* @param beatLength the beat length
*/
public tpSlider(HitObject hitObject, float sliderMultiplier, float beatLength) {
this.startTime = hitObject.getTime();
this.sliderTime = (int) hitObject.getSliderTime(sliderMultiplier, beatLength);
this.curve = hitObject.getSliderCurve(false);
}
/**
* Returns the time duration of a slider segment, in milliseconds.
*/
public int getSegmentLength() { return sliderTime; }
/**
* Returns the coordinates of the slider at a given track position.
* @param time the track position
*/
public Vec2f getPositionAtTime(int time) {
float t = (time - startTime) / sliderTime;
float floor = (float) Math.floor(t);
t = (floor % 2 == 0) ? t - floor : floor + 1 - t;
return curve.pointAt(t);
}
}

View File

@@ -1,86 +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.beatmap;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.util.Log;
/**
* LRU cache for beatmap background images.
*/
public class BeatmapImageCache {
/** Maximum number of cached images. */
private static final int MAX_CACHE_SIZE = 10;
/** Map of all loaded background images. */
private LinkedHashMap<File, Image> cache;
/**
* Constructor.
*/
@SuppressWarnings("serial")
public BeatmapImageCache() {
this.cache = new LinkedHashMap<File, Image>(MAX_CACHE_SIZE + 1, 1.1f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<File, Image> eldest) {
if (size() > MAX_CACHE_SIZE) {
// destroy the eldest image
Image img = eldest.getValue();
if (img != null && !img.isDestroyed()) {
try {
img.destroy();
} catch (SlickException e) {
Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e);
}
}
return true;
}
return false;
}
};
}
/**
* Returns the image mapped to the specified beatmap.
* @param beatmap the Beatmap
* @return the Image, or {@code null} if no such mapping exists
*/
public Image get(Beatmap beatmap) { return cache.get(beatmap.bg); }
/**
* Creates a mapping from the specified beatmap to the given image.
* @param beatmap the Beatmap
* @param image the Image
* @return the previously mapped Image, or {@code null} if no such mapping existed
*/
public Image put(Beatmap beatmap, Image image) { return cache.put(beatmap.bg, image); }
/**
* Removes all entries from the cache.
* <p>
* NOTE: This does NOT destroy the images in the cache, and will cause
* memory leaks if all images have not been destroyed.
*/
public void clear() { cache.clear(); }
}

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
@@ -83,6 +84,10 @@ public class BeatmapParser {
// create a new BeatmapSetList
BeatmapSetList.create();
// create a new watch service
if (Options.isWatchServiceEnabled())
BeatmapWatchService.create();
// parse all directories
parseDirectories(root.listFiles());
}
@@ -110,6 +115,9 @@ public class BeatmapParser {
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // loaded from parser
// watch service
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
// parse directories
BeatmapSetNode lastNode = null;
for (File dir : dirs) {
@@ -134,17 +142,19 @@ public class BeatmapParser {
// 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());
if (map != null) {
Long lastModified = map.get(path);
if (lastModified != null) {
// check last modified times
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.
@@ -162,6 +172,8 @@ public class BeatmapParser {
if (!beatmaps.isEmpty()) {
beatmaps.trimToSize();
allBeatmaps.add(beatmaps);
if (ws != null)
ws.registerAll(dir.toPath());
}
// stop parsing files (interrupted)
@@ -676,10 +688,15 @@ public class BeatmapParser {
beatmap.objects[objectIndex++] = hitObject;
} catch (Exception e) {
Log.warn(String.format("Failed to read hit object '%s' for Beatmap '%s'.",
Log.warn(String.format("Failed to read hit object '%s' for beatmap '%s'.",
line, beatmap.toString()), e);
}
}
// check that all objects were parsed
if (objectIndex != beatmap.objects.length)
ErrorHandler.error(String.format("Parsed %d objects for beatmap '%s', %d objects expected.",
objectIndex, beatmap.toString(), beatmap.objects.length), null, true);
} catch (IOException e) {
ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
}
@@ -711,6 +728,7 @@ public class BeatmapParser {
/**
* Returns the file extension of a file.
* @param file the file name
*/
public static String getExtension(String file) {
int i = file.lastIndexOf('.');

View File

@@ -21,14 +21,15 @@ package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/**
* Data type containing all beatmaps in a beatmap set.
*/
public class BeatmapSet {
public class BeatmapSet implements Iterable<Beatmap> {
/** List of associated beatmaps. */
private ArrayList<Beatmap> beatmaps;
private final ArrayList<Beatmap> beatmaps;
/**
* Constructor.
@@ -46,7 +47,7 @@ public class BeatmapSet {
/**
* Returns the beatmap at the given index.
* @param index the beatmap index
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public Beatmap get(int index) { return beatmaps.get(index); }
@@ -54,10 +55,13 @@ public class BeatmapSet {
* Removes the beatmap at the given index.
* @param index the beatmap index
* @return the removed beatmap
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public Beatmap remove(int index) { return beatmaps.remove(index); }
@Override
public Iterator<Beatmap> iterator() { return beatmaps.iterator(); }
/**
* Returns an array of strings containing beatmap information.
* <ul>
@@ -65,10 +69,10 @@ public class BeatmapSet {
* <li>1: Mapped by {Creator}
* <li>2: Length: {} BPM: {} Objects: {}
* <li>3: Circles: {} Sliders: {} Spinners: {}
* <li>4: CS:{} HP:{} AR:{} OD:{}
* <li>4: CS:{} HP:{} AR:{} OD:{} Stars:{}
* </ul>
* @param index the beatmap index
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public String[] getInfo(int index) {
Beatmap beatmap = beatmaps.get(index);
@@ -88,11 +92,12 @@ public class BeatmapSet {
(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",
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f%s",
Math.min(beatmap.circleSize * multiplier, 10f),
Math.min(beatmap.HPDrainRate * multiplier, 10f),
Math.min(beatmap.approachRate * multiplier, 10f),
Math.min(beatmap.overallDifficulty * multiplier, 10f));
Math.min(beatmap.overallDifficulty * multiplier, 10f),
(beatmap.starRating >= 0) ? String.format(" Stars:%.2f", beatmap.starRating) : "");
return info;
}
@@ -126,7 +131,7 @@ public class BeatmapSet {
return true;
// search: version, tags (remaining beatmaps)
for (int i = 1; i < beatmaps.size(); i++) {
for (int i = 1, n = beatmaps.size(); i < n; i++) {
beatmap = beatmaps.get(i);
if (beatmap.version.toLowerCase().contains(query) ||
beatmap.tags.contains(query))
@@ -138,7 +143,7 @@ public class BeatmapSet {
/**
* Checks whether the beatmap set matches a given condition.
* @param type the condition type (ar, cs, od, hp, bpm, length)
* @param type the condition type (ar, cs, od, hp, bpm, length, star/stars)
* @param operator the operator {@literal (=/==, >, >=, <, <=)}
* @param value the value
* @return true if the condition is met
@@ -154,6 +159,8 @@ public class BeatmapSet {
case "hp": v = beatmap.HPDrainRate; break;
case "bpm": v = beatmap.bpmMax; break;
case "length": v = beatmap.endTime / 1000; break;
case "star":
case "stars": v = Math.round(beatmap.starRating * 100) / 100f; break;
default: return false;
}

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.db.BeatmapDB;
@@ -44,7 +45,7 @@ public class BeatmapSetList {
/** Search pattern for conditional expressions. */
private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
"(ar|cs|od|hp|bpm|length)(=|==|>|>=|<|<=)((\\d*\\.)?\\d+)"
"(ar|cs|od|hp|bpm|length|stars?)(==?|>=?|<=?)((\\d*\\.)?\\d+)"
);
/** List containing all parsed nodes. */
@@ -163,12 +164,17 @@ public class BeatmapSetList {
}
// remove all node references
Beatmap beatmap = node.getBeatmapSet().get(0);
BeatmapSet beatmapSet = node.getBeatmapSet();
Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index);
parsedNodes.remove(eCur);
mapCount -= node.getBeatmapSet().size();
mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID);
for (Beatmap bm : beatmapSet) {
if (bm.md5Hash != null)
this.beatmapHashDB.remove(bm.md5Hash);
}
// reset indices
for (int i = index, size = size(); i < size; i++)
@@ -199,11 +205,16 @@ public class BeatmapSetList {
BeatmapDB.delete(dir.getName());
// delete the associated directory
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
try {
Utils.deleteToTrash(dir);
} catch (IOException e) {
ErrorHandler.error("Could not delete song group.", e, true);
}
if (ws != null)
ws.resume();
return true;
}
@@ -235,6 +246,8 @@ public class BeatmapSetList {
// remove song reference
Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
mapCount--;
if (beatmap.md5Hash != null)
beatmapHashDB.remove(beatmap.md5Hash);
// re-link nodes
if (node.prev != null)
@@ -247,11 +260,16 @@ public class BeatmapSetList {
BeatmapDB.delete(file.getParentFile().getName(), file.getName());
// delete the associated file
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
try {
Utils.deleteToTrash(file);
} catch (IOException e) {
ErrorHandler.error("Could not delete song.", e, true);
}
if (ws != null)
ws.resume();
return true;
}
@@ -312,6 +330,7 @@ public class BeatmapSetList {
/**
* Expands the node at an index by inserting a new node for each Beatmap
* in that node and hiding the group node.
* @param index the node index
* @return the first of the newly-inserted nodes
*/
public BeatmapSetNode expand(int index) {

View File

@@ -21,7 +21,8 @@ package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
@@ -31,7 +32,7 @@ import org.newdawn.slick.Image;
*/
public class BeatmapSetNode {
/** The associated beatmap set. */
private BeatmapSet beatmapSet;
private final BeatmapSet beatmapSet;
/** Index of the selected beatmap (-1 if not focused). */
public int beatmapIndex = -1;
@@ -56,6 +57,14 @@ public class BeatmapSetNode {
*/
public BeatmapSet getBeatmapSet() { return beatmapSet; }
/**
* Returns the selected beatmap (based on {@link #beatmapIndex}).
* @return the beatmap, or null if the index is invalid
*/
public Beatmap getSelectedBeatmap() {
return (beatmapIndex < 0 || beatmapIndex >= beatmapSet.size()) ? null : beatmapSet.get(beatmapIndex);
}
/**
* Draws the button.
* @param x the x coordinate
@@ -78,16 +87,16 @@ public class BeatmapSetNode {
bgColor = Color.white;
textColor = Options.getSkin().getSongSelectActiveTextColor();
} else
bgColor = Utils.COLOR_BLUE_BUTTON;
bgColor = Colors.BLUE_BUTTON;
beatmap = beatmapSet.get(beatmapIndex);
} else {
bgColor = Utils.COLOR_ORANGE_BUTTON;
bgColor = Colors.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;
float cy = y + (bg.getHeight() * 0.18f) - 3;
// draw grade
if (grade != Grade.NULL) {
@@ -98,15 +107,58 @@ public class BeatmapSetNode {
// draw text
if (Options.useUnicodeMetadata()) { // load glyphs
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.DEFAULT, beatmap.artistUnicode);
}
Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
Fonts.MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
Fonts.DEFAULT.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() - 3,
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,
Fonts.BOLD.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 6,
beatmap.version, textColor);
// draw stars
// (note: in osu!, stars are also drawn for beatmap sets of size 1)
if (expanded) {
if (beatmap.starRating >= 0) {
Image star = GameImage.STAR.getImage();
float starOffset = star.getWidth() * 1.7f;
float starX = cx + starOffset * 0.04f;
float starY = cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() * 2 - 8f * GameImage.getUIscale();
float starCenterY = starY + star.getHeight() / 2f;
final float baseAlpha = focus ? 1f : 0.8f;
final float smallStarScale = 0.4f;
star.setAlpha(baseAlpha);
int i = 1;
for (; i < beatmap.starRating && i <= 5; i++) {
if (focus)
star.drawFlash(starX + (i - 1) * starOffset, starY, star.getWidth(), star.getHeight(), textColor);
else
star.draw(starX + (i - 1) * starOffset, starY);
}
if (i <= 5) {
float partialStarScale = smallStarScale + (float) (beatmap.starRating - i + 1) * (1f - smallStarScale);
Image partialStar = star.getScaledCopy(partialStarScale);
partialStar.setAlpha(baseAlpha);
float partialStarY = starCenterY - partialStar.getHeight() / 2f;
if (focus)
partialStar.drawFlash(starX + (i - 1) * starOffset, partialStarY, partialStar.getWidth(), partialStar.getHeight(), textColor);
else
partialStar.draw(starX + (i - 1) * starOffset, partialStarY);
}
if (++i <= 5) {
Image smallStar = star.getScaledCopy(smallStarScale);
smallStar.setAlpha(0.5f);
float smallStarY = starCenterY - smallStar.getHeight() / 2f;
for (; i <= 5; i++) {
if (focus)
smallStar.drawFlash(starX + (i - 1) * starOffset, smallStarY, smallStar.getWidth(), smallStar.getHeight(), textColor);
else
smallStar.draw(starX + (i - 1) * starOffset, smallStarY);
}
}
}
}
}
/**

View File

@@ -39,13 +39,13 @@ public enum BeatmapSortOrder {
LENGTH (4, "Length", new LengthOrder());
/** The ID of the sort (used for tab positioning). */
private int id;
private final int id;
/** The name of the sort. */
private String name;
private final String name;
/** The comparator for the sort. */
private Comparator<BeatmapSetNode> comparator;
private final Comparator<BeatmapSetNode> comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab;
@@ -123,13 +123,11 @@ public enum BeatmapSortOrder {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vMax = 0, wMax = 0;
for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
Beatmap beatmap = v.getBeatmapSet().get(i);
for (Beatmap beatmap : v.getBeatmapSet()) {
if (beatmap.endTime > vMax)
vMax = beatmap.endTime;
}
for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
Beatmap beatmap = w.getBeatmapSet().get(i);
for (Beatmap beatmap : w.getBeatmapSet()) {
if (beatmap.endTime > wMax)
wMax = beatmap.endTime;
}

View File

@@ -0,0 +1,295 @@
/*
* 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.Options;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.newdawn.slick.util.Log;
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Watches the beatmap directory tree for changes.
*
* @author The Java Tutorials (http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java) (base)
*/
public class BeatmapWatchService {
/** Beatmap watcher service instance. */
private static BeatmapWatchService ws;
/**
* Creates a new watch service instance (overwriting any previous instance),
* registers the beatmap directory, and starts processing events.
*/
public static void create() {
// close the existing watch service
destroy();
// create a new watch service
try {
ws = new BeatmapWatchService();
ws.register(Options.getBeatmapDir().toPath());
} catch (IOException e) {
ErrorHandler.error("An I/O exception occurred while creating the watch service.", e, true);
return;
}
// start processing events
ws.start();
}
/**
* Destroys the watch service instance, if any.
* Subsequent calls to {@link #get()} will return {@code null}.
*/
public static void destroy() {
if (ws == null)
return;
try {
ws.watcher.close();
ws.service.shutdownNow();
ws = null;
} catch (IOException e) {
ws = null;
ErrorHandler.error("An I/O exception occurred while closing the previous watch service.", e, true);
}
}
/**
* Returns the single instance of this class.
*/
public static BeatmapWatchService get() { return ws; }
/** Watch service listener interface. */
public interface BeatmapWatchServiceListener {
/**
* Indication that an event was received.
* @param kind the event kind
* @param child the child directory
*/
public void eventReceived(WatchEvent.Kind<?> kind, Path child);
}
/** The list of listeners. */
private static final List<BeatmapWatchServiceListener> listeners = new ArrayList<BeatmapWatchServiceListener>();
/**
* Adds a listener.
* @param listener the listener to add
*/
public static void addListener(BeatmapWatchServiceListener listener) { listeners.add(listener); }
/**
* Removes a listener.
* @param listener the listener to remove
*/
public static void removeListener(BeatmapWatchServiceListener listener) { listeners.remove(listener); }
/**
* Removes all listeners.
*/
public static void removeListeners() { listeners.clear(); }
/** The watch service. */
private final WatchService watcher;
/** The WatchKey -> Path mapping for registered directories. */
private final Map<WatchKey, Path> keys;
/** The Executor. */
private ExecutorService service;
/** Whether the watch service is paused (i.e. does not fire events). */
private boolean paused = false;
/**
* Creates the WatchService.
* @throws IOException if an I/O error occurs
*/
private BeatmapWatchService() throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new ConcurrentHashMap<WatchKey, Path>();
}
/**
* Register the given directory with the WatchService.
* @param dir the directory to register
* @throws IOException if an I/O error occurs
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the WatchService.
* @param start the root directory to register
*/
public void registerAll(final Path start) {
try {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
try {
register(dir);
} catch (IOException e) {
Log.warn(String.format("Failed to register path '%s' with the watch service.", dir.toString()), e);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
Log.warn(String.format("Failed to register paths from root directory '%s' with the watch service.", start.toString()), e);
}
}
@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
/**
* Start processing events in a new thread.
*/
private void start() {
if (service != null)
return;
this.service = Executors.newCachedThreadPool();
service.submit(new Runnable() {
@Override
public void run() { ws.processEvents(); }
});
}
/**
* Process all events for keys queued to the watcher
*/
private void processEvents() {
while (true) {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException | ClosedWatchServiceException e) {
return;
}
Path dir = keys.get(key);
if (dir == null)
continue;
boolean isPaused = paused;
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW)
continue;
// context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
//System.out.printf("%s: %s\n", kind.name(), child);
// fire listeners
if (!isPaused) {
for (BeatmapWatchServiceListener listener : listeners)
listener.eventReceived(kind, child);
}
// if directory is created, then register it and its sub-directories
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS))
registerAll(child);
}
}
// reset key and remove from set if directory no longer accessible
if (!key.reset()) {
keys.remove(key);
if (keys.isEmpty())
break; // all directories are inaccessible
}
}
}
/**
* Stops listener events from being fired.
*/
public void pause() { paused = true; }
/**
* Resumes firing listener events.
*/
public void resume() { paused = false; }
}

View File

@@ -19,6 +19,11 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.objects.curves.Vec2f;
import java.text.DecimalFormat;
import java.text.NumberFormat;
@@ -34,13 +39,6 @@ public class HitObject {
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,
@@ -101,9 +99,18 @@ public class HitObject {
/** Hit sound type (SOUND_* bitmask). */
private byte hitSound;
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
/** Hit sound addition (sampleSet, AdditionSampleSet). */
private byte[] addition;
/** Addition custom sample index. */
private byte additionCustomSampleIndex;
/** Addition hit sound volume. */
private int additionHitSoundVolume;
/** Addition hit sound file. */
private String additionHitSound;
/** Slider curve type (SLIDER_* constant). */
private char sliderType;
@@ -250,9 +257,17 @@ public class HitObject {
// 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]);
if (additionTokens.length > 1) {
this.addition = new byte[2];
addition[0] = Byte.parseByte(additionTokens[0]);
addition[1] = Byte.parseByte(additionTokens[1]);
}
if (additionTokens.length > 2)
this.additionCustomSampleIndex = Byte.parseByte(additionTokens[2]);
if (additionTokens.length > 3)
this.additionHitSoundVolume = Integer.parseInt(additionTokens[3]);
if (additionTokens.length > 4)
this.additionHitSound = additionTokens[4];
}
}
@@ -298,13 +313,13 @@ public class HitObject {
*/
public String getTypeName() {
if (isCircle())
return CIRCLE;
return "circle";
else if (isSlider())
return SLIDER;
return "slider";
else if (isSpinner())
return SPINNER;
return "spinner";
else
return UNKNOWN;
return "unknown object type";
}
/**
@@ -386,6 +401,35 @@ public class HitObject {
*/
public float getPixelLength() { return pixelLength; }
/**
* Returns the time duration of the slider (excluding repeats), in milliseconds.
* @param sliderMultiplier the beatmap's slider movement speed multiplier
* @param beatLength the beat length
* @return the slider segment length
*/
public float getSliderTime(float sliderMultiplier, float beatLength) {
return beatLength * (pixelLength / sliderMultiplier) / 100f;
}
/**
* Returns the slider curve.
* @param scaled whether to use scaled coordinates
* @return a new Curve instance
*/
public Curve getSliderCurve(boolean scaled) {
if (sliderType == SLIDER_PASSTHROUGH && sliderX.length == 2) {
Vec2f nora = new Vec2f(sliderX[0] - x, sliderY[0] - y).nor();
Vec2f norb = new Vec2f(sliderX[0] - sliderX[1], sliderY[0] - sliderY[1]).nor();
if (Math.abs(norb.x * nora.y - norb.y * nora.x) < 0.00001f)
return new LinearBezier(this, false, scaled); // vectors parallel, use linear bezier instead
else
return new CircumscribedCircle(this, scaled);
} else if (sliderType == SLIDER_CATMULL)
return new CatmullCurve(this, scaled);
else
return new LinearBezier(this, sliderType == SLIDER_LINEAR, scaled);
}
/**
* Returns the spinner end time.
* @return the end time (in ms)
@@ -471,6 +515,21 @@ public class HitObject {
return 0;
}
/**
* Returns the custom sample index (addition).
*/
public byte getCustomSampleIndex() { return additionCustomSampleIndex; }
/**
* Returns the hit sound volume (addition).
*/
public int getHitSoundVolume() { return additionHitSoundVolume; }
/**
* Returns the hit sound file (addition).
*/
public String getHitSoundFile() { return additionHitSound; }
/**
* Sets the hit object index in the current stack.
* @param stack index in the stack
@@ -529,9 +588,12 @@ public class HitObject {
// addition
if (addition != null) {
for (int i = 0; i < addition.length; i++) {
sb.append(addition[i]);
sb.append(':');
sb.append(addition[i]); sb.append(':');
}
sb.append(additionCustomSampleIndex); sb.append(':');
sb.append(additionHitSoundVolume); sb.append(':');
if (additionHitSound != null)
sb.append(additionHitSound);
} else
sb.setLength(sb.length() - 1);

View File

@@ -0,0 +1,179 @@
/*
* 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.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.ImageDataFactory;
import org.newdawn.slick.opengl.LoadableImageData;
import org.newdawn.slick.util.Log;
/**
* Simple threaded image loader for a single image file.
*/
public class ImageLoader {
/** The image file. */
private final File file;
/** The loaded image. */
private Image image;
/** The image data. */
private LoadedImageData data;
/** The image loader thread. */
private Thread loaderThread;
/** ImageData wrapper, needed because {@code ImageIOImageData} doesn't implement {@code getImageBufferData()}. */
private class LoadedImageData implements ImageData {
/** The image data implementation. */
private final ImageData imageData;
/** The stored image. */
private final ByteBuffer buffer;
/**
* Constructor.
* @param imageData the class holding the image properties
* @param buffer the stored image
*/
public LoadedImageData(ImageData imageData, ByteBuffer buffer) {
this.imageData = imageData;
this.buffer = buffer;
}
@Override public int getDepth() { return imageData.getDepth(); }
@Override public int getWidth() { return imageData.getWidth(); }
@Override public int getHeight() { return imageData.getHeight();}
@Override public int getTexWidth() { return imageData.getTexWidth(); }
@Override public int getTexHeight() { return imageData.getTexHeight(); }
@Override public ByteBuffer getImageBufferData() { return buffer; }
}
/** Image loading thread. */
private class ImageLoaderThread extends Thread {
/** The image file input stream. */
private BufferedInputStream in;
@Override
public void interrupt() {
super.interrupt();
if (in != null) {
try {
in.close(); // interrupt I/O
} catch (IOException e) {}
}
}
@Override
public void run() {
// load image data into a ByteBuffer to use constructor Image(ImageData)
LoadableImageData imageData = ImageDataFactory.getImageDataFor(file.getAbsolutePath());
try (BufferedInputStream in = this.in = new BufferedInputStream(new FileInputStream(file))) {
ByteBuffer textureBuffer = imageData.loadImage(in, false, null);
if (!isInterrupted())
data = new LoadedImageData(imageData, textureBuffer);
} catch (IOException e) {
if (!isInterrupted())
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
this.in = null;
}
}
/**
* Constructor. Call {@link ImageLoader#load(boolean)} to load the image.
* @param file the image file
*/
public ImageLoader(File file) {
this.file = file;
}
/**
* Loads the image.
* @param threaded true to load the image data in a new thread
*/
public void load(boolean threaded) {
if (!file.isFile())
return;
if (threaded) {
if (loaderThread != null && loaderThread.isAlive())
loaderThread.interrupt();
loaderThread = new ImageLoaderThread();
loaderThread.start();
} else {
try {
image = new Image(file.getAbsolutePath());
} catch (SlickException e) {
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
}
}
/**
* Returns the image.
* @return the loaded image, or null if not loaded
*/
public Image getImage() {
if (image == null && data != null) {
image = new Image(data);
data = null;
}
return image;
}
/**
* Returns whether an image is currently loading in another thread.
* @return true if loading, false otherwise
*/
public boolean isLoading() { return (loaderThread != null && loaderThread.isAlive()); }
/**
* Interrupts the image loader, if running.
*/
public void interrupt() {
if (isLoading())
loaderThread.interrupt();
}
/**
* Releases all resources.
*/
public void destroy() {
interrupt();
loaderThread = null;
if (image != null && !image.isDestroyed()) {
try {
image.destroy();
} catch (SlickException e) {
Log.warn(String.format("Failed to destroy image '%s'.", image.getResourceReference()), e);
}
image = null;
}
data = null;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.util.LinkedHashMap;
import java.util.Map;
/**
* Least recently used cache.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
@SuppressWarnings("serial")
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
/** The cache capacity. */
private final int capacity;
/**
* Creates a least recently used cache with the given capacity.
* @param capacity the capacity
*/
public LRUCache(int capacity) {
super(capacity + 1, 1.1f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
if (size() > capacity) {
eldestRemoved(eldest);
return true;
}
return false;
}
/**
* Notification that the eldest entry was removed.
* Can be used to clean up any resources when this happens (via override).
* @param eldest the removed entry
*/
public void eldestRemoved(Map.Entry<K, V> eldest) {}
}

View File

@@ -16,7 +16,10 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu;
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import java.io.File;
import java.io.FilenameFilter;
@@ -62,6 +65,9 @@ public class OszUnpacker {
}
// unpack OSZs
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
for (File file : files) {
fileIndex++;
String dirName = file.getName().substring(0, file.getName().lastIndexOf('.'));
@@ -73,6 +79,8 @@ public class OszUnpacker {
dirs.add(songDir);
}
}
if (ws != null)
ws.resume();
fileIndex = -1;
files = null;

View File

@@ -43,7 +43,7 @@ public class BeatmapDB {
* Current database version.
* This value should be changed whenever the database format changes.
*/
private static final String DATABASE_VERSION = "2015-06-11";
private static final String DATABASE_VERSION = "2015-09-02";
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@@ -58,7 +58,7 @@ public class BeatmapDB {
private static Connection connection;
/** Query statements. */
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, updateSizeStmt;
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, setStarsStmt, updateSizeStmt;
/** Current size of beatmap cache table. */
private static int cacheSize = -1;
@@ -95,12 +95,13 @@ public class BeatmapDB {
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 = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
}
@@ -123,7 +124,7 @@ public class BeatmapDB {
"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, " +
"md5hash TEXT" +
"md5hash TEXT, stars REAL" +
"); " +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
@@ -342,6 +343,7 @@ public class BeatmapDB {
stmt.setString(39, beatmap.breaksToString());
stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash);
stmt.setDouble(42, beatmap.starRating);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -484,6 +486,7 @@ public class BeatmapDB {
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = rs.getString(41);
beatmap.starRating = rs.getDouble(42);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -571,6 +574,25 @@ public class BeatmapDB {
}
}
/**
* Sets the star rating for a beatmap in the database.
* @param beatmap the beatmap
*/
public static void setStars(Beatmap beatmap) {
if (connection == null)
return;
try {
setStarsStmt.setDouble(1, beatmap.starRating);
setStarsStmt.setString(2, beatmap.getFile().getParentFile().getName());
setStarsStmt.setString(3, beatmap.getFile().getName());
setStarsStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error(String.format("Failed to save star rating '%.4f' for beatmap '%s' in database.",
beatmap.starRating, beatmap.toString()), e, true);
}
}
/**
* Closes the connection to the database.
*/

View File

@@ -119,7 +119,9 @@ public class ScoreDB {
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"replay = ? AND playerName = ?"
"(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " +
"(playerName = ? OR (playerName IS NULL AND ? IS NULL))"
// TODO: extra playerName checks not needed if name is guaranteed not null
);
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare score statements.", e, true);
@@ -222,6 +224,7 @@ public class ScoreDB {
try {
setStatementFields(insertStmt, data);
insertStmt.setString(18, data.replayString);
insertStmt.setString(19, data.playerName);
insertStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to save score to database.", e, true);
@@ -238,6 +241,10 @@ public class ScoreDB {
try {
setStatementFields(deleteScoreStmt, data);
deleteScoreStmt.setString(18, data.replayString);
deleteScoreStmt.setString(19, data.replayString);
deleteScoreStmt.setString(20, data.playerName);
deleteScoreStmt.setString(21, data.playerName);
deleteScoreStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to delete score from database.", e, true);
@@ -289,8 +296,6 @@ public class ScoreDB {
stmt.setInt(15, data.combo);
stmt.setBoolean(16, data.perfect);
stmt.setInt(17, data.mods);
stmt.setString(18, data.replayString);
stmt.setString(19, data.playerName);
}
/**

View File

@@ -46,6 +46,9 @@ public class Download {
/** Read timeout, in ms. */
public static final int READ_TIMEOUT = 10000;
/** Maximum number of HTTP/HTTPS redirects to follow. */
public static final int MAX_REDIRECTS = 3;
/** Time between download speed and ETA updates, in ms. */
private static final int UPDATE_INTERVAL = 1000;
@@ -58,7 +61,7 @@ public class Download {
ERROR ("Error");
/** The status name. */
private String name;
private final String name;
/**
* Constructor.
@@ -172,13 +175,57 @@ public class Download {
new Thread() {
@Override
public void run() {
// open connection, get content length
// open connection
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
URL downloadURL = url;
int redirectCount = 0;
boolean isRedirect = false;
do {
isRedirect = false;
conn = (HttpURLConnection) downloadURL.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
// allow HTTP <--> HTTPS redirects
// http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html
conn.setInstanceFollowRedirects(false);
conn.setRequestProperty("User-Agent", "Mozilla/5.0...");
// check for redirect
int status = conn.getResponseCode();
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM ||
status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) {
URL base = conn.getURL();
String location = conn.getHeaderField("Location");
URL target = null;
if (location != null)
target = new URL(base, location);
conn.disconnect();
// check for problems
String error = null;
if (location == null)
error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString());
else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https"))
error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol());
else if (redirectCount > MAX_REDIRECTS)
error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS);
if (error != null) {
ErrorHandler.error(error, null, false);
throw new IOException();
}
// follow redirect
downloadURL = target;
redirectCount++;
isRedirect = true;
}
} while (isRedirect);
// store content length
contentLength = conn.getContentLength();
} catch (IOException e) {
status = Status.ERROR;
@@ -198,9 +245,18 @@ public class Download {
fos = fileOutputStream;
status = Status.DOWNLOADING;
updateReadSoFar();
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
long bytesRead = fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
if (status == Status.DOWNLOADING) { // not interrupted
// TODO: if connection is lost before a download finishes, it's still marked as "complete"
// check if the entire file was received
if (bytesRead < contentLength) {
status = Status.ERROR;
Log.warn(String.format("Download '%s' failed: %d bytes expected, %d bytes received.", url.toString(), contentLength, bytesRead));
if (listener != null)
listener.error();
return;
}
// mark download as complete
status = Status.COMPLETE;
rbc.close();
fos.close();
@@ -273,7 +329,7 @@ public class Download {
public long readSoFar() {
switch (status) {
case COMPLETE:
return contentLength;
return (rbc != null) ? rbc.getReadSoFar() : contentLength;
case DOWNLOADING:
if (rbc != null)
return rbc.getReadSoFar();

View File

@@ -75,7 +75,7 @@ public class DownloadList {
}
/**
* Returns the size of the doownloads list.
* Returns the size of the downloads list.
*/
public int size() { return nodes.size(); }

View File

@@ -26,6 +26,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.io.File;
@@ -42,19 +44,19 @@ public class DownloadNode {
private Download download;
/** Beatmap set ID. */
private int beatmapSetID;
private final int beatmapSetID;
/** Last updated date string. */
private String date;
private final String date;
/** Song title. */
private String title, titleUnicode;
private final String title, titleUnicode;
/** Song artist. */
private String artist, artistUnicode;
private final String artist, artistUnicode;
/** Beatmap creator. */
private String creator;
private final String creator;
/** Button drawing values. */
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
@@ -68,12 +70,6 @@ public class DownloadNode {
/** Container width. */
private static int containerWidth;
/** Button background colors. */
public static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param width the container width
@@ -86,16 +82,16 @@ public class DownloadNode {
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
buttonHeight = Fonts.MEDIUM.getLineHeight() * 2.1f;
buttonOffset = buttonHeight * 1.1f;
// download info
infoBaseX = width * 0.75f;
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
infoBaseY = height * 0.07f + Fonts.LARGE.getLineHeight() * 2f;
infoWidth = width * 0.25f;
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
infoHeight = Fonts.DEFAULT.getLineHeight() * 2.4f;
float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight();
float searchY = (height * 0.05f) + Fonts.LARGE.getLineHeight();
float buttonHeight = height * 0.038f;
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
@@ -228,10 +224,9 @@ public class DownloadNode {
* @param total the total number of buttons
*/
public static void drawResultScrollbar(Graphics g, float position, float total) {
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset,
buttonBaseX, buttonBaseY,
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, buttonBaseX, buttonBaseY,
buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight,
BG_NORMAL, Color.white, true);
Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
@@ -242,11 +237,18 @@ public class DownloadNode {
*/
public static void drawDownloadScrollbar(Graphics g, float index, float total) {
UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY,
infoWidth, maxDownloadsShown * infoHeight, BG_NORMAL, Color.white, true);
infoWidth, maxDownloadsShown * infoHeight, Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
* Constructor.
* @param beatmapSetID the beatmap set ID
* @param date the last modified date string
* @param title the song title
* @param titleUnicode the Unicode song title (or {@code null} if none)
* @param artist the song artist
* @param artistUnicode the Unicode song artist (or {@code null} if none)
* @param creator the beatmap creator
*/
public DownloadNode(int beatmapSetID, String date, String title,
String titleUnicode, String artist, String artistUnicode, String creator) {
@@ -273,7 +275,7 @@ public class DownloadNode {
return;
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
this.download = new Download(url, path, rename);
Download download = new Download(url, path, rename);
download.setListener(new DownloadListener() {
@Override
public void completed() {
@@ -285,8 +287,9 @@ public class DownloadNode {
UI.sendBarNotification("Download failed due to a connection error.");
}
});
this.download = download;
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null);
Fonts.loadGlyphs(Fonts.LARGE, getTitle());
}
/**
@@ -348,12 +351,12 @@ public class DownloadNode {
Download dl = DownloadList.get().getDownload(beatmapSetID);
// rectangle outline
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
g.setColor((focus) ? Colors.BLACK_BG_FOCUS : (hover) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// map is already loaded
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
g.setColor(Utils.COLOR_BLUE_BUTTON);
g.setColor(Colors.BLUE_BUTTON);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
}
@@ -361,7 +364,7 @@ public class DownloadNode {
if (dl != null) {
float progress = dl.getProgress();
if (progress > 0f) {
g.setColor(Utils.COLOR_GREEN);
g.setColor(Colors.GREEN);
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
}
}
@@ -373,21 +376,22 @@ public class DownloadNode {
// text
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
if (Options.useUnicodeMetadata()) { // load glyphs
Fonts.loadGlyphs(Fonts.BOLD, getTitle());
Fonts.loadGlyphs(Fonts.BOLD, getArtist());
}
// TODO can't set clip again or else old clip will be cleared
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight());
Utils.FONT_BOLD.drawString(
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Fonts.DEFAULT.getWidth(creator)), Fonts.BOLD.getLineHeight());
Fonts.BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
//g.clearClip();
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
Fonts.DEFAULT.drawString(
textX, y + marginY + Fonts.BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
Fonts.DEFAULT.drawString(
edgeX - Fonts.DEFAULT.getWidth(creator), y + marginY,
creator, Color.white);
}
@@ -399,6 +403,7 @@ public class DownloadNode {
* @param hover true if the mouse is hovering over this button
*/
public void drawDownload(Graphics g, float position, int id, boolean hover) {
Download download = this.download; // in case clearDownload() is called asynchronously
if (download == null) {
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
return;
@@ -410,7 +415,7 @@ public class DownloadNode {
float marginY = infoHeight * 0.04f;
// rectangle outline
g.setColor((id % 2 == 0) ? BG_HOVER : BG_NORMAL);
g.setColor((id % 2 == 0) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
// text
@@ -428,8 +433,8 @@ public class DownloadNode {
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
}
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
Fonts.BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Fonts.DEFAULT.drawString(textX, y + marginY + Fonts.BOLD.getLineHeight(), info, Color.white);
// 'x' button
if (hover) {

View File

@@ -28,7 +28,7 @@ import java.nio.channels.ReadableByteChannel;
*/
public class ReadableByteChannelWrapper implements ReadableByteChannel {
/** The wrapped ReadableByteChannel. */
private ReadableByteChannel rbc;
private final ReadableByteChannel rbc;
/** The number of bytes read. */
private long bytesRead;

View File

@@ -77,7 +77,7 @@ public class Updater {
UPDATE_FINAL ("Update queued.");
/** The status description. */
private String description;
private final String description;
/**
* Constructor.
@@ -194,9 +194,10 @@ public class Updater {
/**
* Checks the program version against the version file on the update server.
* @throws IOException if an I/O exception occurs
*/
public void checkForUpdates() throws IOException {
if (status != Status.INITIAL || System.getProperty("XDG") != null)
if (status != Status.INITIAL || Options.USE_XDG)
return;
status = Status.CHECKING;

View File

@@ -75,4 +75,7 @@ public abstract class DownloadServer {
public String getPreviewURL(int beatmapSetID) {
return String.format(PREVIEW_URL, beatmapSetID);
}
@Override
public String toString() { return getName(); }
}

View File

@@ -0,0 +1,202 @@
/*
* 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.util.ArrayList;
import java.util.List;
/**
* Download server: http://osu.mengsky.net/
*/
public class MengSkyServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "MengSky";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.mengsky.net/d.php?id=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.mengsky.net/index.php?search_keywords=%s";
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "http://osu.mengsky.net/index.php?next=1&page=%d";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 20;
/** Total result count from the last query. */
private int totalResults = -1;
/** Constructor. */
public MengSkyServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, page - 1);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Maybe an HTML parser would be better for this...
// FORMAT:
// <div class="beatmap" style="{{...}}">
// <div class="preview" style="background-image:url(http://b.ppy.sh/thumb/{{id}}l.jpg)"></div>
// <div class="name"> <a href="">{{artist}} - {{title}}</a> </div>
// <div class="douban_details">
// <span>Creator:</span> {{creator}}<br>
// <span>MaxBpm:</span> {{bpm}}<br>
// <span>Title:</span> {{titleUnicode}}<br>
// <span>Artist:</span> {{artistUnicode}}<br>
// <span>Status:</span> <font color={{"#00CD00" || "#EE0000"}}>{{"Ranked?" || "Unranked"}}</font><br>
// </div>
// <div class="details"> <a href=""></a> <br>
// <span>Fork:</span> bloodcat<br>
// <span>UpdateTime:</span> {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}<br>
// <span>Mode:</span> <img id="{{'s' || 'c' || ...}}" src="/img/{{'s' || 'c' || ...}}.png"> {{...}}
// </div>
// <div class="download">
// <a href="https://osu.ppy.sh/s/{{id}}" class=" btn" target="_blank">Osu.ppy</a>
// </div>
// <div class="download">
// <a href="http://osu.mengsky.net/d.php?id={{id}}" class=" btn" target="_blank">DownLoad</a>
// </div>
// </div>
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String
START_TAG = "<div class=\"beatmap\"", NAME_TAG = "<div class=\"name\"> <a href=\"\">",
CREATOR_TAG = "<span>Creator:</span> ", TITLE_TAG = "<span>Title:</span> ", ARTIST_TAG = "<span>Artist:</span> ",
TIMESTAMP_TAG = "<span>UpdateTime:</span> ", DOWNLOAD_TAG = "<div class=\"download\">",
BR_TAG = "<br>", HREF_TAG = "<a href=\"", HREF_TAG_END = "</a>";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
int divCount = 0;
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
divCount++;
int i, j;
// find beatmap
i = html.indexOf(NAME_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + NAME_TAG.length(), j);
String[] beatmapTokens = beatmap.split(" - ", 2);
if (beatmapTokens.length < 2)
continue;
String artist = beatmapTokens[0];
String title = beatmapTokens[1];
// find other beatmap details
i = html.indexOf(CREATOR_TAG, j + HREF_TAG_END.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + CREATOR_TAG.length());
if (j == -1 || j > n) continue;
String creator = html.substring(i + CREATOR_TAG.length(), j);
i = html.indexOf(TITLE_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + TITLE_TAG.length());
if (j == -1 || j > n) continue;
String titleUnicode = html.substring(i + TITLE_TAG.length(), j);
i = html.indexOf(ARTIST_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + ARTIST_TAG.length());
if (j == -1 || j > n) continue;
String artistUnicode = html.substring(i + ARTIST_TAG.length(), j);
i = html.indexOf(TIMESTAMP_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(BR_TAG, i + TIMESTAMP_TAG.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + TIMESTAMP_TAG.length(), j);
// find beatmap ID
i = html.indexOf(DOWNLOAD_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
i = html.indexOf(HREF_TAG, i + DOWNLOAD_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf('"', i + HREF_TAG.length());
if (j == -1 || j > n) continue;
String downloadURL = html.substring(i + HREF_TAG.length(), j);
String[] downloadTokens = downloadURL.split("(?=\\d*$)", 2);
if (downloadTokens[1].isEmpty()) continue;
int id;
try {
id = Integer.parseInt(downloadTokens[1]);
} catch (NumberFormatException e) {
continue;
}
nodeList.add(new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
else {
int resultCount = nodes.length + (page - 1) * PAGE_LIMIT;
if (divCount == PAGE_LIMIT)
resultCount++;
this.totalResults = resultCount;
}
} 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 2; }
@Override
public int totalResults() { return totalResults; }
}

View File

@@ -0,0 +1,133 @@
/*
* 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.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Download server: http://osu.uu.gl/
*/
public class MnetworkServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "Mnetwork";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.uu.gl/s/%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.uu.gl/d/%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Beatmap pattern. */
private Pattern BEATMAP_PATTERN = Pattern.compile("^(\\d+) ([^-]+) - (.+)\\.osz$");
/** Constructor. */
public MnetworkServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String queryString = (query.isEmpty()) ? "-" : query;
String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8"));
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Not using a full HTML parser because this is a relatively simple operation.
// FORMAT:
// <div class="tr_title">
// <b><a href='/s/{{id}}'>{{id}} {{artist}} - {{title}}.osz</a></b><br />
// BPM: {{bpm}} <b>|</b> Total Time: {{m}}:{{s}}<br/>
// Genre: {{genre}} <b>|</b> Updated: {{MMM}} {{d}}, {{yyyy}}<br />
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String START_TAG = "<div class=\"tr_title\">", HREF_TAG = "<a href=", HREF_TAG_END = "</a>", UPDATED = "Updated: ";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
int i, j;
// find beatmap
i = html.indexOf(HREF_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
i = html.indexOf('>', i + HREF_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + 1, j).trim();
// find date
i = html.indexOf(UPDATED, j);
if (i == -1 || i >= n) continue;
j = html.indexOf('<', i + UPDATED.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + UPDATED.length(), j).trim();
// parse id, title, and artist
Matcher m = BEATMAP_PATTERN.matcher(beatmap);
if (!m.matches())
continue;
nodeList.add(new DownloadNode(Integer.parseInt(m.group(1)), date, m.group(3), null, m.group(2), null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
this.totalResults = nodes.length;
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
}
return nodes;
}
@Override
public int minQueryLength() { return 0; }
@Override
public int totalResults() { return totalResults; }
}

View File

@@ -39,6 +39,8 @@ import org.json.JSONObject;
/**
* Download server: http://loli.al/
* <p>
* <i>This server went offline in August 2015.</i>
*/
public class OsuMirrorServer extends DownloadServer {
/** Server name. */

View File

@@ -0,0 +1,204 @@
/*
* 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.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.json.JSONObject;
/**
* Download server: http://osu.yas-online.net/
*/
public class YaSOnlineServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "YaS Online";
/** Formatted download URL (returns JSON): {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "https://osu.yas-online.net/json.mapdata.php?mapId=%d";
/**
* Formatted download fetch URL: {@code downloadLink}
* (e.g. {@code /fetch/49125122158ef360a66a07bce2d0483596913843-m-10418.osz})
*/
private static final String DOWNLOAD_FETCH_URL = "https://osu.yas-online.net%s";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 25;
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "https://osu.yas-online.net/json.maplist.php?o=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "https://osu.yas-online.net/json.search.php?searchQuery=%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Max server download ID seen (for approximating total pages). */
private int maxServerID = 0;
/** Constructor. */
public YaSOnlineServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
try {
// TODO: do this asynchronously (will require lots of changes...)
return getDownloadURLFromMapData(beatmapSetID);
} catch (IOException e) {
return null;
}
}
/**
* Returns the beatmap download URL by downloading its map data.
* <p>
* This is needed because there is no other way to find a beatmap's direct
* download URL.
* @param beatmapSetID the beatmap set ID
* @return the URL string, or null if the address could not be determined
* @throws IOException if any connection error occurred
*/
private String getDownloadURLFromMapData(int beatmapSetID) throws IOException {
try {
// read JSON
String search = String.format(DOWNLOAD_URL, beatmapSetID);
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
JSONObject results;
if (json == null ||
!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
return null;
}
// parse result
Iterator<?> keys = results.keys();
if (!keys.hasNext())
return null;
String key = (String) keys.next();
JSONObject item = results.getJSONObject(key);
String downloadLink = item.getString("downloadLink");
return String.format(DOWNLOAD_FETCH_URL, downloadLink);
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true);
return null;
}
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read JSON
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, (page - 1) * PAGE_LIMIT);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
if (json == null) {
this.totalResults = -1;
return null;
}
JSONObject results;
if (!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
this.totalResults = 0;
return new DownloadNode[0];
}
// parse result list
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
for (Object obj : results.keySet()) {
String key = (String) obj;
JSONObject item = results.getJSONObject(key);
// parse title and artist
String title, artist;
String str = item.getString("map");
int index = str.indexOf(" - ");
if (index > -1) {
title = str.substring(0, index);
artist = str.substring(index + 3);
} else { // should never happen...
title = str;
artist = "?";
}
// only contains date added if part of a beatmap pack
int added = item.getInt("added");
String date = (added == 0) ? "?" : formatDate(added);
// approximate page count
int serverID = item.getInt("id");
if (serverID > maxServerID)
maxServerID = serverID;
nodeList.add(new DownloadNode(item.getInt("mapid"), date, title, null, artist, null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
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 3; }
@Override
public int totalResults() { return totalResults; }
/**
* Returns a formatted date string from a raw date.
* @param timestamp the UTC timestamp, in seconds
* @return the formatted date
*/
private String formatDate(int timestamp) {
Date d = new Date(timestamp * 1000L);
DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
return fmt.format(d);
}
}

View File

@@ -61,6 +61,7 @@ public class OsuReader {
/**
* Closes the input stream.
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException { reader.close(); }

View File

@@ -62,7 +62,7 @@ public class OsuWriter {
/**
* Closes the output stream.
* @throws IOException
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException { writer.close(); }

View File

@@ -25,7 +25,9 @@ import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -35,9 +37,6 @@ import org.newdawn.slick.Graphics;
* Data type representing a circle object.
*/
public class Circle implements GameObject {
/** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375;
/** The diameter of hit circles. */
private static float diameter;
@@ -62,11 +61,10 @@ public class Circle implements GameObject {
/**
* Initializes the Circle data type with map modifiers, images, and dimensions.
* @param container the game container
* @param circleSize the map's circleSize value
* @param circleDiameter the circle diameter
*/
public static void init(GameContainer container, float circleSize) {
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
public static void init(GameContainer container, float circleDiameter) {
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
@@ -93,26 +91,37 @@ public class Circle implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
float scale = timeDiff / (float) game.getApproachTime();
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
if (GameMod.HIDDEN.isActive()) {
final int hiddenDecayTime = game.getHiddenDecayTime();
final int hiddenTimeDiff = game.getHiddenTimeDiff();
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
alpha = Math.min(alpha, hiddenAlpha);
}
}
if (timeDiff >= 0)
float oldAlpha = Colors.WHITE_FADE.a;
Colors.WHITE_FADE.a = color.a = alpha;
if (timeDiff >= 0 && !GameMod.HIDDEN.isActive())
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).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, Colors.WHITE_FADE);
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -186,7 +195,7 @@ public class Circle implements GameObject {
}
@Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import org.newdawn.slick.Graphics;
@@ -53,7 +54,7 @@ public class DummyObject implements GameObject {
public boolean mousePressed(int x, int y, int trackPosition) { return false; }
@Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }

View File

@@ -18,6 +18,8 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.objects.curves.Vec2f;
import org.newdawn.slick.Graphics;
/**
@@ -55,9 +57,9 @@ public interface GameObject {
/**
* Returns the coordinates of the hit object at a given track position.
* @param trackPosition the track position
* @return the [x,y] coordinates
* @return the position vector
*/
public float[] getPointAt(int trackPosition);
public Vec2f getPointAt(int trackPosition);
/**
* Returns the end time of the hit object.

View File

@@ -26,11 +26,10 @@ import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -56,9 +55,6 @@ public class Slider implements GameObject {
/** The diameter of hit circles. */
private static float diameter;
/** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375;
/** The associated HitObject. */
private HitObject hitObject;
@@ -113,22 +109,21 @@ public class Slider implements GameObject {
/**
* Initializes the Slider data type with images and dimensions.
* @param container the game container
* @param circleSize the map's circleSize value
* @param circleDiameter the circle diameter
* @param beatmap the associated beatmap
*/
public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
public static void init(GameContainer container, float circleDiameter, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
followRadius = diameter / 2 * 3f;
// slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
if (GameImage.SLIDER_BALL.hasBeatmapSkinImages() ||
(!GameImage.SLIDER_BALL.hasBeatmapSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
sliderBallImages = GameImage.SLIDER_BALL.getImages();
else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
@@ -160,7 +155,7 @@ public class Slider implements GameObject {
updatePosition();
// slider time calculations
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
this.sliderTime = hitObject.getSliderTime(sliderMultiplier, game.getBeatLength());
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
// ticks
@@ -178,36 +173,46 @@ public class Slider implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
float scale = timeDiff / (float) game.getApproachTime();
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
float oldAlpha = Colors.WHITE_FADE.a;
Colors.WHITE_FADE.a = color.a = alpha;
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
float[] endPos = curve.pointAt(1);
Vec2f endPos = curve.pointAt(1);
curve.draw(color);
color.a = alpha;
// end circle
hitCircle.drawCentered(endPos[0], endPos[1], color);
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
hitCircle.drawCentered(endPos.x, endPos.y, color);
hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE);
// start circle
hitCircle.drawCentered(x, y, color);
if (!overlayAboveNumber)
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// ticks
if (ticksT != null) {
Image tick = GameImage.SLIDER_TICK.getImage();
for (int i = 0; i < ticksT.length; i++) {
float[] c = curve.pointAt(ticksT[i]);
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
Vec2f c = curve.pointAt(ticksT[i]);
tick.drawCentered(c.x, c.y, Colors.WHITE_FADE);
}
}
if (GameMod.HIDDEN.isActive()) {
final int hiddenDecayTime = game.getHiddenDecayTime();
final int hiddenTimeDiff = game.getHiddenTimeDiff();
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
alpha = Math.min(alpha, hiddenAlpha);
}
}
if (sliderClickedInitial)
@@ -216,7 +221,7 @@ public class Slider implements GameObject {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// repeats
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
@@ -232,7 +237,7 @@ public class Slider implements GameObject {
if (tcurRepeat % 2 == 0) {
// last circle
arrow.setRotation(curve.getEndAngle());
arrow.drawCentered(endPos[0], endPos[1]);
arrow.drawCentered(endPos.x, endPos.y);
} else {
// first circle
arrow.setRotation(curve.getStartAngle());
@@ -243,40 +248,41 @@ public class Slider implements GameObject {
if (timeDiff >= 0) {
// approach circle
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
if (!GameMod.HIDDEN.isActive())
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
} else {
// Since update() might not have run before drawing during a replay, the
// slider time may not have been calculated, which causes NAN numbers and flicker.
if (sliderTime == 0)
return;
float[] c = curve.pointAt(getT(trackPosition, false));
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
Vec2f c = curve.pointAt(getT(trackPosition, false));
Vec2f c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
float t = getT(trackPosition, false);
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
float angle = (float) (Math.atan2(c2.y - c.y, c2.x - c.x) * 180 / Math.PI);
sliderBallFrame.setRotation(angle);
sliderBallFrame.drawCentered(c[0], c[1]);
sliderBallFrame.drawCentered(c.x, c.y);
// follow circle
if (followCircleActive) {
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]);
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c.x, c.y);
// "flashlight" mod: dim the screen
if (GameMod.FLASHLIGHT.isActive()) {
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = 0.75f;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = 0.75f;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, containerWidth, containerHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
Colors.BLACK_ALPHA.a = oldAlphaBlack;
}
}
}
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -346,9 +352,9 @@ public class Slider implements GameObject {
float cx, cy;
HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle
float[] lastPos = curve.pointAt(1);
cx = lastPos[0];
cy = lastPos[1];
Vec2f lastPos = curve.pointAt(1);
cx = lastPos.x;
cy = lastPos.y;
type = HitObjectType.SLIDER_LAST;
} else { // first circle
cx = x;
@@ -429,8 +435,8 @@ public class Slider implements GameObject {
// check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) {
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
Vec2f c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (distance < followRadius)
sliderHeldToEnd = true;
}
@@ -473,12 +479,11 @@ public class Slider implements GameObject {
}
// holding slider...
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
Vec2f c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
// mouse pressed and within follow circle
followCircleActive = true;
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
// held during new repeat
if (isNewRepeat) {
@@ -489,14 +494,14 @@ public class Slider implements GameObject {
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
} else // first circle
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
c[0], c[1], hitObject, currentRepeats);
c.x, c.y, hitObject, currentRepeats);
}
// held during new tick
if (isNewTick) {
ticksHit++;
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
c[0], c[1], hitObject, currentRepeats);
c.x, c.y, hitObject, currentRepeats);
}
// held near end of slider
@@ -518,22 +523,16 @@ public class Slider implements GameObject {
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
if (hitObject.getSliderType() == HitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
this.curve = new CircumscribedCircle(hitObject, color);
else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
this.curve = new CatmullCurve(hitObject, color);
else
this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
this.curve = hitObject.getSliderCurve(true);
}
@Override
public float[] getPointAt(int trackPosition) {
public Vec2f getPointAt(int trackPosition) {
if (trackPosition <= hitObject.getTime())
return new float[] { x, y };
return new Vec2f(x, y);
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
if (hitObject.getRepeatCount() % 2 == 0)
return new float[] { x, y };
return new Vec2f(x, y);
else
return curve.pointAt(1);
} else

View File

@@ -27,7 +27,9 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -50,9 +52,6 @@ public class Spinner implements GameObject {
/** The amount of time, in milliseconds, before another velocity is stored. */
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
/** The amount of time, in milliseconds, to fade in the spinner. */
private static final int FADE_IN_TIME = 500;
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
private static final float
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
@@ -69,6 +68,9 @@ public class Spinner implements GameObject {
/** The associated HitObject. */
private HitObject hitObject;
/** The associated Game object. */
private Game game;
/** The associated GameData object. */
private GameData data;
@@ -124,6 +126,7 @@ public class Spinner implements GameObject {
*/
public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
this.game = game;
this.data = data;
/*
@@ -162,7 +165,7 @@ public class Spinner implements GameObject {
final int maxVel = 48;
final int minTime = 2000;
final int maxTime = 5000;
maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
* (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
storedDeltaAngle = new float[maxStoredDeltaAngles];
@@ -175,20 +178,21 @@ public class Spinner implements GameObject {
public void draw(Graphics g, int trackPosition) {
// only draw spinners shortly before start time
int timeDiff = hitObject.getTime() - trackPosition;
if (timeDiff - FADE_IN_TIME > 0)
final int fadeInTime = game.getFadeInTime();
if (timeDiff - fadeInTime > 0)
return;
boolean spinnerComplete = (rotations >= rotationsNeeded);
float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
float alpha = Utils.clamp(1 - (float) timeDiff / fadeInTime, 0f, 1f);
// darken screen
if (Options.getSkin().isSpinnerFadePlayfield()) {
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
float oldAlpha = Colors.BLACK_ALPHA.a;
if (timeDiff > 0)
Utils.COLOR_BLACK_ALPHA.a *= alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
Colors.BLACK_ALPHA.a *= alpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Colors.BLACK_ALPHA.a = oldAlpha;
}
// rpm
@@ -210,13 +214,15 @@ public class Spinner implements GameObject {
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
// main spinner elements
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
GameImage.SPINNER_CIRCLE.getImage().setAlpha(alpha);
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
approachCircleScaled.setAlpha(alpha);
approachCircleScaled.drawCentered(width / 2, height / 2);
if (!GameMod.HIDDEN.isActive()) {
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
approachCircleScaled.setAlpha(alpha);
approachCircleScaled.drawCentered(width / 2, height / 2);
}
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
@@ -342,7 +348,7 @@ public class Spinner implements GameObject {
public void updatePosition() {}
@Override
public float[] getPointAt(int trackPosition) {
public Vec2f getPointAt(int trackPosition) {
// get spinner time
int timeDiff;
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
@@ -357,10 +363,7 @@ public class Spinner implements GameObject {
float multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
float angle = (timeDiff * multiplier) - HALF_PI;
final float r = height / 10f;
return new float[] {
(float) (x + r * Math.cos(angle)),
(float) (y + r * Math.sin(angle))
};
return new Vec2f((float) (x + r * Math.cos(angle)), (float) (y + r * Math.sin(angle)));
}
@Override

View File

@@ -18,14 +18,10 @@
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.
*
@@ -35,10 +31,18 @@ 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);
public CatmullCurve(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public CatmullCurve(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
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
@@ -53,24 +57,15 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
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);
}
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
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);
}
}
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)
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
init(catmulls);
}

View File

@@ -18,8 +18,6 @@
package itdelatrisu.opsu.objects.curves;
import org.newdawn.slick.SlickException;
/**
* Representation of a Centripetal CatmullRom spline.
* (Currently not technically Centripetal CatmullRom.)
@@ -37,11 +35,10 @@ public class CentripetalCatmullRom extends CurveType {
/**
* Constructor.
* @param points the control points of the curve
* @throws SlickException
*/
protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
protected CentripetalCatmullRom(Vec2f[] points) {
if (points.length != 4)
throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
throw new RuntimeException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
this.points = points;
time = new float[4];

View File

@@ -18,11 +18,9 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import org.newdawn.slick.Color;
/**
* Representation of a curve along a Circumscribed Circle of three points.
* http://en.wikipedia.org/wiki/Circumscribed_circle
@@ -53,10 +51,18 @@ public class CircumscribedCircle extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
*/
public CircumscribedCircle(HitObject hitObject, Color color) {
super(hitObject, color);
public CircumscribedCircle(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public CircumscribedCircle(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
// construct the three points
this.start = new Vec2f(getX(0), getY(0));
@@ -70,8 +76,6 @@ public class CircumscribedCircle extends Curve {
Vec2f norb = mid.cpy().sub(end).nor();
this.circleCenter = intersect(mida, nora, midb, norb);
if (circleCenter == null)
return;
// find the angles relative to the circle center
Vec2f startAngPoint = start.cpy().sub(circleCenter);
@@ -92,13 +96,8 @@ public class CircumscribedCircle extends Curve {
startAng -= TWO_PI;
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
endAng -= TWO_PI;
else {
ErrorHandler.error(
String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
startAng, midAng, endAng), null, true
);
return;
}
else
throw new RuntimeException(String.format("Cannot find angles between midAng (%.3f %.3f %.3f).", startAng, midAng, endAng));
}
// find an angle with an arc length of pixelLength along this circle
@@ -116,10 +115,8 @@ public class CircumscribedCircle extends Curve {
// calculate points
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
curve = new Vec2f[(int) step + 1];
for (int i = 0; i < curve.length; i++) {
float[] xy = pointAt(i / step);
curve[i] = new Vec2f(xy[0], xy[1]);
}
for (int i = 0; i < curve.length; i++)
curve[i] = pointAt(i / step);
}
/**
@@ -151,21 +148,19 @@ public class CircumscribedCircle extends Curve {
//u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x);
float des = tb.x * ta.y - tb.y * ta.x;
if (Math.abs(des) < 0.00001f) {
ErrorHandler.error("Vectors are parallel.", null, true);
return null;
}
if (Math.abs(des) < 0.00001f)
throw new RuntimeException("Vectors are parallel.");
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
return b.cpy().add(tb.x * u, tb.y * u);
}
@Override
public float[] pointAt(float t) {
float ang = lerp(startAng, endAng, t);
return new float[] {
public Vec2f pointAt(float t) {
float ang = Utils.lerp(startAng, endAng, t);
return new Vec2f(
(float) (Math.cos(ang) * radius + circleCenter.x),
(float) (Math.sin(ang) * radius + circleCenter.y)
};
);
}
@Override

View File

@@ -20,10 +20,10 @@ package itdelatrisu.opsu.objects.curves;
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 itdelatrisu.opsu.ui.Colors;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.GLContext;
@@ -64,14 +64,21 @@ public abstract class Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
* @param scaled whether to use scaled coordinates
*/
protected Curve(HitObject hitObject, Color color) {
protected Curve(HitObject hitObject, boolean scaled) {
this.hitObject = hitObject;
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
if (scaled) {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
} else {
this.x = hitObject.getX();
this.y = hitObject.getY();
this.sliderX = hitObject.getSliderX();
this.sliderY = hitObject.getSliderY();
}
this.renderState = null;
}
@@ -80,28 +87,28 @@ public abstract class Curve {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
* @param circleSize the circle size
* @param circleDiameter the circle diameter
* @param borderColor the curve border color
*/
public static void init(int width, int height, float circleSize, Color borderColor) {
public static void init(int width, int height, float circleDiameter, Color borderColor) {
Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities();
mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32;
mmsliderSupported = capabilities.GL_EXT_framebuffer_object;
if (mmsliderSupported)
CurveRenderState.init(width, height, circleSize);
CurveRenderState.init(width, height, circleDiameter);
else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
Log.warn("New slider style requires FBO support and OpenGL 3.2.");
Log.warn("New slider style requires FBO support.");
}
}
/**
* Returns the point on the curve at a value t.
* @param t the t value [0, 1]
* @return the point [x, y]
* @return the position vector
*/
public abstract float[] pointAt(float t);
public abstract Vec2f pointAt(float t);
/**
* Draws the full curve to the graphics context.
@@ -116,7 +123,7 @@ public abstract class Curve {
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);
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE);
for (int i = 0; i < curve.length; i++)
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
}
@@ -151,13 +158,6 @@ public abstract class Curve {
*/
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
/**
* Linear interpolation of a and b at t.
*/
protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
/**
* Discards the slider cache (only used for mmsliders).
*/

View File

@@ -18,13 +18,12 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.Utils;
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
@@ -41,10 +40,18 @@ public abstract class EqualDistanceMultiCurve extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
*/
public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
super(hitObject, color);
public EqualDistanceMultiCurve(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public EqualDistanceMultiCurve(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
}
/**
@@ -94,7 +101,7 @@ public abstract class EqualDistanceMultiCurve extends Curve {
// 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));
curve[i] = new Vec2f(Utils.lerp(lastCurve.x, thisCurve.x, t), Utils.lerp(lastCurve.y, thisCurve.y, t));
} else
curve[i] = thisCurve;
}
@@ -117,20 +124,19 @@ public abstract class EqualDistanceMultiCurve extends Curve {
}
@Override
public float[] pointAt(float t) {
public Vec2f 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 {
if (index >= ncurve)
return curve[ncurve].cpy();
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)
};
return new Vec2f(
Utils.lerp(poi.x, poi2.x, t2),
Utils.lerp(poi.y, poi2.y, t2)
);
}
}

View File

@@ -22,8 +22,6 @@ import itdelatrisu.opsu.beatmap.HitObject;
import java.util.LinkedList;
import org.newdawn.slick.Color;
/**
* Representation of Bezier curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
@@ -34,11 +32,20 @@ public class LinearBezier extends EqualDistanceMultiCurve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
* @param line whether a new curve should be generated for each sequential pair
*/
public LinearBezier(HitObject hitObject, Color color, boolean line) {
super(hitObject, color);
public LinearBezier(HitObject hitObject, boolean line) {
this(hitObject, line, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param line whether a new curve should be generated for each sequential pair
* @param scaled whether to use scaled coordinates
*/
public LinearBezier(HitObject hitObject, boolean line, boolean scaled) {
super(hitObject, scaled);
LinkedList<CurveType> beziers = new LinkedList<CurveType>();

View File

@@ -40,6 +40,16 @@ public class Vec2f {
*/
public Vec2f() {}
/**
* Sets the x and y components of this vector.
* @return itself (for chaining)
*/
public Vec2f set(float nx, float ny) {
x = nx;
y = ny;
return this;
}
/**
* Finds the midpoint between this vector and another vector.
* @param o the other vector
@@ -93,6 +103,17 @@ public class Vec2f {
return this;
}
/**
* Turns this vector into a unit vector.
* @return itself (for chaining)
*/
public Vec2f normalize() {
float len = len();
x /= len;
y /= len;
return this;
}
/**
* Returns a copy of this vector.
*/

View File

@@ -18,20 +18,21 @@
package itdelatrisu.opsu.render;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.ui.Colors;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.EXTFramebufferObject;
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;
@@ -63,15 +64,14 @@ public class CurveRenderState {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
* @param circleSize the circle size
* @param circleDiameter the circle diameter
*/
public static void init(int width, int height, float circleSize) {
public static void init(int width, int height, float circleDiameter) {
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 = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
FrameBufferCache.init(width, height);
}
@@ -114,20 +114,25 @@ public class CurveRenderState {
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);
int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
//glGetInteger requires a buffer of size 16, even though just 4
//values are returned in this specific case
IntBuffer oldViewport = BufferUtils.createIntBuffer(16);
GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 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;
Colors.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;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb);
GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3));
Colors.WHITE_FADE.a = alpha;
}
// draw a fullscreen quad with the texture that contains the curve
@@ -389,7 +394,7 @@ public class CurveRenderState {
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);
EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D);
}
}
@@ -402,12 +407,12 @@ public class CurveRenderState {
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"
GL20.glShaderSource(vtxShdr, "#version 110\n"
+ "\n"
+ "layout(location = 0) in vec4 in_position;\n"
+ "layout(location = 1) in vec2 in_tex_coord;\n"
+ "attribute vec4 in_position;\n"
+ "attribute vec2 in_tex_coord;\n"
+ "\n"
+ "out vec2 tex_coord;\n"
+ "varying vec2 tex_coord;\n"
+ "void main()\n"
+ "{\n"
+ " gl_Position = in_position;\n"
@@ -419,22 +424,21 @@ public class CurveRenderState {
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
Log.error("Vertex Shader compilation failed.", new Exception(error));
}
GL20.glShaderSource(frgShdr, "#version 330\n"
GL20.glShaderSource(frgShdr, "#version 110\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"
+ "varying vec2 tex_coord;\n"
+ "\n"
+ "void main()\n"
+ "{\n"
+ " vec4 in_color = texture(tex, tex_coord.x);\n"
+ " vec4 in_color = texture1D(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"
+ " gl_FragColor = new_color;\n"
+ "}");
GL20.glCompileShader(frgShdr);
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);

View File

@@ -19,10 +19,8 @@ package itdelatrisu.opsu.render;
import java.nio.ByteBuffer;
import org.lwjgl.opengl.EXTFramebufferObject;
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.
@@ -50,16 +48,16 @@ public class Rendertarget {
private Rendertarget(int width, int height) {
this.width = width;
this.height = height;
fboID = GL30.glGenFramebuffers();
fboID = EXTFramebufferObject.glGenFramebuffersEXT();
textureID = GL11.glGenTextures();
depthBufferID = GL30.glGenRenderbuffers();
depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT();
}
/**
* Bind this rendertarget as the primary framebuffer.
*/
public void bind() {
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID);
}
/**
@@ -83,7 +81,7 @@ public class Rendertarget {
* Bind the default framebuffer.
*/
public static void unbind() {
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
}
/**
@@ -93,7 +91,7 @@ public class Rendertarget {
* @param height the height
*/
public static Rendertarget createRTTFramebuffer(int width, int height) {
int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING);
int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
Rendertarget buffer = new Rendertarget(width,height);
buffer.bind();
@@ -104,15 +102,14 @@ public class Rendertarget {
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);
GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID);
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID);
EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height);
EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0);
GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer);
return buffer;
}
@@ -122,8 +119,8 @@ public class Rendertarget {
* to use this rendertarget with OpenGL after calling this method.
*/
public void destroyRTT() {
GL30.glDeleteFramebuffers(fboID);
GL30.glDeleteRenderbuffers(depthBufferID);
EXTFramebufferObject.glDeleteFramebuffersEXT(fboID);
EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID);
GL11.glDeleteTextures(textureID);
}
}

View File

@@ -25,10 +25,10 @@ package itdelatrisu.opsu.replay;
*/
public class LifeFrame {
/** Time. */
private int time;
private final int time;
/** Percentage. */
private float percentage;
private final float percentage;
/**
* Constructor.

View File

@@ -35,14 +35,14 @@ public enum PlaybackSpeed {
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
/** The button image. */
private GameImage gameImage;
private final GameImage gameImage;
/** The playback speed modifier. */
private final float modifier;
/** The button. */
private MenuButton button;
/** The playback speed modifier. */
private float modifier;
/** Enum values. */
private static PlaybackSpeed[] values = PlaybackSpeed.values();

View File

@@ -41,11 +41,11 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lzma.streams.LzmaOutputStream;
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
import org.newdawn.slick.util.Log;
import lzma.streams.LzmaOutputStream;
/**
* Captures osu! replay data.
* https://osu.ppy.sh/wiki/Osr_%28file_format%29

View File

@@ -38,13 +38,13 @@ public class ReplayFrame {
private int timeDiff;
/** Time, in milliseconds. */
private int time;
private final int time;
/** Cursor coordinates (in OsuPixels). */
private float x, y;
private final float x, y;
/** Keys pressed (bitmask). */
private int keys;
private final int keys;
/**
* Returns the start frame.
@@ -81,7 +81,8 @@ public class ReplayFrame {
public int getTimeDiff() { return timeDiff; }
/**
* Sets the time since the previous action, in milliseconds.
* Sets the time since the previous action.
* @param diff the time difference, in milliseconds
*/
public void setTimeDiff(int diff) { this.timeDiff = diff; }

View File

@@ -29,8 +29,11 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.List;
@@ -187,15 +190,15 @@ public class ButtonMenu extends BasicGameState {
float mult = GameMod.getScoreMultiplier();
String multString = String.format("Score Multiplier: %.2fx", mult);
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
Utils.FONT_LARGE.drawString(
(width - Utils.FONT_LARGE.getWidth(multString)) / 2f,
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
Fonts.LARGE.drawString(
(width - Fonts.LARGE.getWidth(multString)) / 2f,
multY, multString, multColor);
// category text
for (GameMod.Category category : GameMod.Category.values()) {
Utils.FONT_LARGE.drawString(category.getX(),
category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f,
Fonts.LARGE.drawString(category.getX(),
category.getY() - Fonts.LARGE.getLineHeight() / 2f,
category.getName(), category.getColor());
}
@@ -217,7 +220,7 @@ public class ButtonMenu extends BasicGameState {
}
// tooltips
if (hoverMod != null && hoverMod.isImplemented())
if (hoverMod != null)
UI.updateTooltip(delta, hoverMod.getDescription(), true);
}
@@ -253,7 +256,7 @@ public class ButtonMenu extends BasicGameState {
};
/** The buttons in the state. */
private Button[] buttons;
private final Button[] buttons;
/** The associated MenuButton objects. */
private MenuButton[] menuButtons;
@@ -261,8 +264,11 @@ public class ButtonMenu extends BasicGameState {
/** The actual title string list, generated upon entering the state. */
private List<String> actualTitle;
/** The horizontal center offset, used for the initial button animation. */
private AnimatedValue centerOffset;
/** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */
private static final float OFFSET_WIDTH_RATIO = 1 / 18f;
private static final float OFFSET_WIDTH_RATIO = 1 / 25f;
/**
* Constructor.
@@ -288,7 +294,7 @@ public class ButtonMenu extends BasicGameState {
menuButtons = new MenuButton[buttons.length];
for (int i = 0; i < buttons.length; i++) {
MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY));
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Utils.FONT_XLARGE, Color.white);
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Fonts.XLARGE, Color.white);
b.setHoverFade();
menuButtons[i] = b;
}
@@ -301,7 +307,7 @@ public class ButtonMenu extends BasicGameState {
*/
protected float getBaseY(GameContainer container, StateBasedGame game) {
float baseY = container.getHeight() * 0.2f;
baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight());
baseY += ((getTitle(container, game).length - 1) * Fonts.LARGE.getLineHeight());
return baseY;
}
@@ -315,9 +321,9 @@ public class ButtonMenu extends BasicGameState {
// draw title
if (actualTitle != null) {
float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
int lineHeight = Utils.FONT_LARGE.getLineHeight();
int lineHeight = Fonts.LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++)
Utils.FONT_LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
Fonts.LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
}
// draw buttons
@@ -336,18 +342,14 @@ public class ButtonMenu extends BasicGameState {
*/
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
float center = container.getWidth() / 2f;
boolean centerOffsetUpdated = centerOffset.update(delta);
float centerOffsetX = centerOffset.getValue();
for (int i = 0; i < buttons.length; i++) {
menuButtons[i].hoverUpdate(delta, mouseX, mouseY);
// move button to center
float x = menuButtons[i].getX();
if (i % 2 == 0) {
if (x < center)
menuButtons[i].setX(Math.min(x + (delta / 5f), center));
} else {
if (x > center)
menuButtons[i].setX(Math.max(x - (delta / 5f), center));
}
if (centerOffsetUpdated)
menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX);
}
}
@@ -404,9 +406,10 @@ public class ButtonMenu extends BasicGameState {
*/
public void enter(GameContainer container, StateBasedGame game) {
float center = container.getWidth() / 2f;
float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO;
float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO;
centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE);
for (int i = 0; i < buttons.length; i++) {
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset));
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1));
menuButtons[i].resetHover();
}
@@ -416,8 +419,8 @@ public class ButtonMenu extends BasicGameState {
int maxLineWidth = (int) (container.getWidth() * 0.96f);
for (int i = 0; i < title.length; i++) {
// wrap text if too long
if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
List<String> list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) {
List<String> list = Fonts.wrap(Fonts.LARGE, title[i], maxLineWidth);
actualTitle.addAll(list);
} else
actualTitle.add(title[i]);
@@ -545,10 +548,10 @@ public class ButtonMenu extends BasicGameState {
};
/** The text to show on the button. */
private String text;
private final String text;
/** The button color. */
private Color color;
private final Color color;
/**
* Constructor.
@@ -591,7 +594,7 @@ public class ButtonMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public ButtonMenu(int state) {
this.state = state;

View File

@@ -21,7 +21,6 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
@@ -29,13 +28,19 @@ import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode;
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.downloads.servers.MengSkyServer;
import itdelatrisu.opsu.downloads.servers.MnetworkServer;
import itdelatrisu.opsu.downloads.servers.YaSOnlineServer;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.DropdownMenu;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KinecticScrolling;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -75,10 +80,10 @@ public class DownloadsMenu extends BasicGameState {
private static final int MIN_REQUEST_INTERVAL = 300;
/** Available beatmap download servers. */
private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
/** The beatmap download server index. */
private int serverIndex = 0;
private static final DownloadServer[] SERVERS = {
new BloodcatServer(), new HexideServer(), new YaSOnlineServer(),
new MnetworkServer(), new MengSkyServer()
};
/** The current list of search results. */
private DownloadNode[] resultList;
@@ -137,14 +142,14 @@ public class DownloadsMenu extends BasicGameState {
/** Page direction for last query. */
private Page lastQueryDir = Page.RESET;
/** Number of active requests. */
private int activeRequests = 0;
/** Previous and next page buttons. */
private MenuButton prevPage, nextPage;
/** Buttons. */
private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
private MenuButton clearButton, importButton, resetButton, rankedButton;
/** Dropdown menu. */
private DropdownMenu<DownloadServer> serverMenu;
/** Beatmap importing thread. */
private Thread importThread;
@@ -155,11 +160,97 @@ public class DownloadsMenu extends BasicGameState {
/** The bar notification to send upon entering the state. */
private String barNotificationOnLoad;
/** Search query, executed in {@code queryThread}. */
private SearchQuery searchQuery;
/** Search query helper class. */
private class SearchQuery implements Runnable {
/** The search query. */
private final String query;
/** The download server. */
private final DownloadServer server;
/** Whether the query was interrupted. */
private boolean interrupted = false;
/** Whether the query has completed execution. */
private boolean complete = false;
/**
* Constructor.
* @param query the search query
* @param server the download server
*/
public SearchQuery(String query, DownloadServer server) {
this.query = query;
this.server = server;
}
/** Interrupt the query and prevent the results from being processed, if not already complete. */
public void interrupt() { interrupted = true; }
/** Returns whether the query has completed execution. */
public boolean isComplete() { return complete; }
@Override
public void run() {
// check page direction
Page lastPageDir = pageDir;
pageDir = Page.RESET;
int lastPageSize = (resultList != null) ? resultList.length : 0;
int newPage = page;
if (lastPageDir == Page.RESET)
newPage = 1;
else if (lastPageDir == Page.NEXT)
newPage++;
else if (lastPageDir == Page.PREVIOUS)
newPage--;
try {
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
if (!interrupted) {
// update page total
page = newPage;
if (nodes != null) {
if (lastPageDir == Page.NEXT)
pageResultTotal += nodes.length;
else if (lastPageDir == Page.PREVIOUS)
pageResultTotal -= lastPageSize;
else if (lastPageDir == Page.RESET)
pageResultTotal = nodes.length;
} else
pageResultTotal = 0;
resultList = nodes;
totalResults = server.totalResults();
focusResult = -1;
startResultPos.setPosition(0);
if (nodes == null)
searchResultString = "An error has occurred.";
else {
if (query.isEmpty())
searchResultString = "Type to search!";
else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found.";
else
searchResultString = String.format("%d result%s found!",
totalResults, (totalResults == 1) ? "" : "s");
}
}
} catch (IOException e) {
if (!interrupted)
searchResultString = "Could not establish connection to server.";
} finally {
complete = true;
}
}
}
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public DownloadsMenu(int state) {
this.state = state;
@@ -175,17 +266,17 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
float baseX = width * 0.024f;
float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
float searchY = (height * 0.04f) + Fonts.LARGE.getLineHeight();
float searchWidth = width * 0.3f;
// search
searchTimer = SEARCH_DELAY;
searchResultString = "Loading data from server...";
search = new TextField(
container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
container, Fonts.DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Fonts.MEDIUM.getLineHeight()
);
search.setBackgroundColor(DownloadNode.BG_NORMAL);
search.setBackgroundColor(Colors.BLACK_BG_NORMAL);
search.setBorderColor(Color.white);
search.setTextColor(Color.white);
search.setConsumeEvents(false);
@@ -208,9 +299,8 @@ public class DownloadsMenu extends BasicGameState {
float buttonHeight = height * 0.038f;
float resetWidth = width * 0.085f;
float rankedWidth = width * 0.15f;
float serverWidth = width * 0.12f;
float lowerWidth = width * 0.12f;
float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
float topButtonY = searchY + Fonts.MEDIUM.getLineHeight() / 2f;
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
Image button = GameImage.MENU_BUTTON_MID.getImage();
Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage();
@@ -220,11 +310,9 @@ public class DownloadsMenu extends BasicGameState {
int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
@@ -234,16 +322,48 @@ public class DownloadsMenu extends BasicGameState {
baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
serverButton = new MenuButton(serverButtonImage, buttonL, buttonR,
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY);
clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white);
resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white);
clearButton.setText("Clear", Fonts.MEDIUM, Color.white);
importButton.setText("Import All", Fonts.MEDIUM, Color.white);
resetButton.setText("Reset", Fonts.MEDIUM, Color.white);
clearButton.setHoverFade();
importButton.setHoverFade();
resetButton.setHoverFade();
rankedButton.setHoverFade();
serverButton.setHoverFade();
// dropdown menu
int serverWidth = (int) (width * 0.12f);
serverMenu = new DropdownMenu<DownloadServer>(container, SERVERS,
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth, searchY, serverWidth) {
@Override
public void itemSelected(int index, DownloadServer item) {
resultList = null;
startResultPos.setPosition(0);
focusResult = -1;
totalResults = 0;
page = 0;
pageResultTotal = 1;
pageDir = Page.RESET;
searchResultString = "Loading data from server...";
lastQuery = null;
pageDir = Page.RESET;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
}
@Override
public boolean menuClicked(int index) {
// block input during beatmap importing
if (importThread != null)
return false;
SoundController.playSound(SoundEffect.MENUCLICK);
return true;
}
};
serverMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
serverMenu.setBorderColor(Color.black);
serverMenu.setChevronRightColor(Color.white);
}
@Override
@@ -252,18 +372,19 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = serverMenu.contains(mouseX, mouseY);
// background
GameImage.SEARCH_BG.getImage().draw();
// title
Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
Fonts.LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
// search
g.setColor(Color.white);
g.setLineWidth(2f);
search.render(container, g);
Utils.FONT_BOLD.drawString(
Fonts.BOLD.drawString(
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
searchResultString, Color.white
);
@@ -283,7 +404,7 @@ public class DownloadsMenu extends BasicGameState {
if (index >= nodes.length)
break;
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
DownloadNode.resultContains(mouseX, mouseY - offset, i),
DownloadNode.resultContains(mouseX, mouseY - offset, i) && !inDropdownMenu,
(index == focusResult), (previewID == nodes[index].getID()));
}
g.clearClip();
@@ -297,9 +418,9 @@ public class DownloadsMenu extends BasicGameState {
float baseX = width * 0.024f;
float buttonY = height * 0.2f;
float buttonWidth = width * 0.7f;
Utils.FONT_BOLD.drawString(
baseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
buttonY - Utils.FONT_BOLD.getLineHeight() * 1.3f,
Fonts.BOLD.drawString(
baseX + (buttonWidth - Fonts.BOLD.getWidth("Page 1")) / 2f,
buttonY - Fonts.BOLD.getLineHeight() * 1.3f,
String.format("Page %d", page), Color.white
);
if (page > 1)
@@ -311,10 +432,10 @@ public class DownloadsMenu extends BasicGameState {
// downloads
float downloadsX = width * 0.75f, downloadsY = search.getY();
g.setColor(DownloadNode.BG_NORMAL);
g.setColor(Colors.BLACK_BG_NORMAL);
g.fillRect(downloadsX, downloadsY,
width * 0.25f, height - downloadsY * 2f);
Utils.FONT_LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
Fonts.LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
int downloadsSize = DownloadList.get().size();
if (downloadsSize > 0) {
int maxDownloadsShown = DownloadNode.maxDownloadsShown();
@@ -344,15 +465,16 @@ public class DownloadsMenu extends BasicGameState {
clearButton.draw(Color.gray);
importButton.draw(Color.orange);
resetButton.draw(Color.red);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Fonts.MEDIUM, Color.white);
rankedButton.draw(Color.magenta);
serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
serverButton.draw(Color.blue);
// dropdown menu
serverMenu.render(container, g);
// importing beatmaps
if (importThread != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -379,7 +501,6 @@ public class DownloadsMenu extends BasicGameState {
importButton.hoverUpdate(delta, mouseX, mouseY);
resetButton.hoverUpdate(delta, mouseX, mouseY);
rankedButton.hoverUpdate(delta, mouseX, mouseY);
serverButton.hoverUpdate(delta, mouseX, mouseY);
if (DownloadList.get() != null)
startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown()));
@@ -399,72 +520,22 @@ public class DownloadsMenu extends BasicGameState {
searchTimer = 0;
searchTimerReset = false;
final String query = search.getText().trim().toLowerCase();
final DownloadServer server = SERVERS[serverIndex];
String query = search.getText().trim().toLowerCase();
DownloadServer server = serverMenu.getSelectedItem();
if ((lastQuery == null || !query.equals(lastQuery)) &&
(query.length() == 0 || query.length() >= server.minQueryLength())) {
lastQuery = query;
lastQueryDir = pageDir;
if (queryThread != null && queryThread.isAlive())
if (queryThread != null && queryThread.isAlive()) {
queryThread.interrupt();
if (searchQuery != null)
searchQuery.interrupt();
}
// execute query
queryThread = new Thread() {
@Override
public void run() {
activeRequests++;
// check page direction
Page lastPageDir = pageDir;
pageDir = Page.RESET;
int lastPageSize = (resultList != null) ? resultList.length : 0;
int newPage = page;
if (lastPageDir == Page.RESET)
newPage = 1;
else if (lastPageDir == Page.NEXT)
newPage++;
else if (lastPageDir == Page.PREVIOUS)
newPage--;
try {
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
if (activeRequests - 1 == 0) {
// update page total
page = newPage;
if (nodes != null) {
if (lastPageDir == Page.NEXT)
pageResultTotal += nodes.length;
else if (lastPageDir == Page.PREVIOUS)
pageResultTotal -= lastPageSize;
else if (lastPageDir == Page.RESET)
pageResultTotal = nodes.length;
} else
pageResultTotal = 0;
resultList = nodes;
totalResults = server.totalResults();
focusResult = -1;
startResultPos.setPosition(0);
if (nodes == null)
searchResultString = "An error has occurred.";
else {
if (query.isEmpty())
searchResultString = "Type to search!";
else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found.";
else
searchResultString = String.format("%d result%s found!",
totalResults, (totalResults == 1) ? "" : "s");
}
}
} catch (IOException e) {
searchResultString = "Could not establish connection to server.";
} finally {
activeRequests--;
queryThread = null;
}
}
};
searchQuery = new SearchQuery(query, server);
queryThread = new Thread(searchQuery);
queryThread.start();
}
}
@@ -474,7 +545,7 @@ public class DownloadsMenu extends BasicGameState {
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))
else if (serverMenu.baseContains(mouseX, mouseY))
UI.updateTooltip(delta, "Select a download server.", false);
}
@@ -534,7 +605,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// play preview
try {
final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
MusicController.pause();
new Thread() {
@Override
@@ -578,7 +649,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// start download
if (!DownloadList.get().contains(node.getID())) {
node.createDownload(SERVERS[serverIndex]);
node.createDownload(serverMenu.getSelectedItem());
if (node.getDownload() == null)
UI.sendBarNotification("The download could not be started.");
else {
@@ -601,23 +672,27 @@ public class DownloadsMenu extends BasicGameState {
// pages
if (nodes.length > 0) {
if (page > 1 && prevPage.contains(x, y)) {
if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive())
if (lastQueryDir == Page.PREVIOUS && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.PREVIOUS;
lastQuery = null;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
}
return;
}
if (pageResultTotal < totalResults && nextPage.contains(x, y)) {
if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive())
if (lastQueryDir == Page.NEXT && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.NEXT;
lastQuery = null;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -670,6 +745,8 @@ public class DownloadsMenu extends BasicGameState {
search.setText("");
lastQuery = null;
pageDir = Page.RESET;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -678,22 +755,8 @@ public class DownloadsMenu extends BasicGameState {
rankedOnly = !rankedOnly;
lastQuery = null;
pageDir = Page.RESET;
resetSearchTimer();
return;
}
if (serverButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUCLICK);
resultList = null;
startResultPos.setPosition(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;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -806,6 +869,8 @@ public class DownloadsMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUCLICK);
lastQuery = null;
pageDir = Page.CURRENT;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
break;
case Input.KEY_F7:
@@ -837,7 +902,8 @@ public class DownloadsMenu extends BasicGameState {
importButton.resetHover();
resetButton.resetHover();
rankedButton.resetHover();
serverButton.resetHover();
serverMenu.activate();
serverMenu.reset();
focusResult = -1;
startResultPos.setPosition(0);
startDownloadIndexPos.setPosition(0);
@@ -853,6 +919,7 @@ public class DownloadsMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
search.setFocus(false);
serverMenu.deactivate();
SoundController.stopTrack();
MusicController.resume();
}

View File

@@ -42,12 +42,16 @@ import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.render.FrameBufferCache;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.LinkedList;
@@ -116,6 +120,15 @@ public class Game extends BasicGameState {
/** Hit object approach time, in milliseconds. */
private int approachTime;
/** The amount of time for hit objects to fade in, in milliseconds. */
private int fadeInTime;
/** Decay time for hit objects in the "Hidden" mod, in milliseconds. */
private int hiddenDecayTime;
/** Time before the hit object time by which the objects have completely faded in the "Hidden" mod, in milliseconds. */
private int hiddenTimeDiff;
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
private int[] hitResultOffset;
@@ -146,7 +159,7 @@ public class Game extends BasicGameState {
countdown2Sound, countdownGoSound;
/** Mouse coordinates before game paused. */
private int pausedMouseX = -1, pausedMouseY = -1;
private Vec2f pausedMousePosition;
/** Track position when game paused. */
private int pauseTime = -1;
@@ -209,7 +222,7 @@ public class Game extends BasicGameState {
private int flashlightRadius;
/** The cursor coordinates using the "auto" or "relax" mods. */
private int autoMouseX = 0, autoMouseY = 0;
private Vec2f autoMousePosition;
/** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed;
@@ -220,11 +233,20 @@ public class Game extends BasicGameState {
/** Whether the game is currently seeking to a replay position. */
private boolean isSeeking;
/** Music position bar coordinates and dimensions (for replay seeking). */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Music position bar background colors. */
private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
MUSICBAR_HOVER = new Color(12, 9, 10, 0.35f),
MUSICBAR_FILL = new Color(255, 255, 255, 0.75f);
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public Game(int state) {
this.state = state;
@@ -245,6 +267,12 @@ public class Game extends BasicGameState {
gOffscreen = offscreen.getGraphics();
gOffscreen.setBackground(Color.black);
// initialize music position bar location
musicBarX = width * 0.01f;
musicBarY = height * 0.05f;
musicBarWidth = Math.max(width * 0.005f, 7);
musicBarHeight = height * 0.9f;
// create the associated GameData object
data = new GameData(width, height);
}
@@ -278,7 +306,7 @@ public class Game extends BasicGameState {
else
dimLevel = 1f;
}
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel);
playfield.draw();
@@ -290,32 +318,31 @@ public class Game extends BasicGameState {
// "auto" and "autopilot" mods: move cursor automatically
// TODO: this should really be in update(), not render()
autoMouseX = width / 2;
autoMouseY = height / 2;
autoMousePosition.set(width / 2, height / 2);
autoMousePressed = false;
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
float[] autoXY = null;
Vec2f autoPoint = null;
if (isLeadIn()) {
// lead-in
float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
autoMouseY = (int) (height / (2f - progress));
autoMousePosition.y = height / (2f - progress);
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
// before first object
timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) {
float[] xy = gameObjects[0].getPointAt(trackPosition);
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
Vec2f point = gameObjects[0].getPointAt(trackPosition);
autoPoint = getPointAt(autoMousePosition.x, autoMousePosition.y, point.x, point.y, 1f - ((float) timeDiff / approachTime));
}
} else if (objectIndex < beatmap.objects.length) {
// normal object
int objectTime = beatmap.objects[objectIndex].getTime();
if (trackPosition < objectTime) {
float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
Vec2f startPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
int startTime = gameObjects[objectIndex - 1].getEndTime();
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
// starting a break: keep cursor at previous hit object position
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
autoXY = xyStart;
autoPoint = startPoint;
// after a break ends: move startTime to break end time
else if (breakIndex > 1) {
@@ -324,10 +351,10 @@ public class Game extends BasicGameState {
startTime = lastBreakEndTime;
}
}
if (autoXY == null) {
float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
if (autoPoint == null) {
Vec2f endPoint = gameObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime;
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
autoPoint = getPointAt(startPoint.x, startPoint.y, endPoint.x, endPoint.y, (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300];
@@ -336,19 +363,17 @@ public class Game extends BasicGameState {
autoMousePressed = true;
}
} else {
autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
autoPoint = gameObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true;
}
} else {
// last object
autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
autoPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
}
// set mouse coordinates
if (autoXY != null) {
autoMouseX = (int) autoXY[0];
autoMouseY = (int) autoXY[1];
}
if (autoPoint != null)
autoMousePosition.set(autoPoint.x, autoPoint.y);
}
// "flashlight" mod: restricted view of hit objects around cursor
@@ -366,12 +391,12 @@ public class Game extends BasicGameState {
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
g.clearAlphaMap();
int mouseX, mouseY;
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
mouseX = pausedMouseX;
mouseY = pausedMouseY;
if (pauseTime > -1 && pausedMousePosition != null) {
mouseX = (int) pausedMousePosition.x;
mouseY = (int) pausedMousePosition.y;
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
mouseX = autoMouseX;
mouseY = autoMouseY;
mouseX = (int) autoMousePosition.x;
mouseY = (int) autoMousePosition.y;
} else if (isReplay) {
mouseX = replayX;
mouseY = replayY;
@@ -455,15 +480,15 @@ public class Game extends BasicGameState {
GameImage.SCOREBAR_BG.getImage().getHeight(),
GameImage.SCOREBAR_KI.getImage().getHeight()
);
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
float oldAlpha = Colors.WHITE_FADE.a;
if (timeDiff < -500)
Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
Utils.FONT_MEDIUM.drawString(
Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f;
Fonts.MEDIUM.drawString(
2 + (width / 100), retryHeight,
String.format("%d retries and counting...", retries),
Utils.COLOR_WHITE_FADE
Colors.WHITE_FADE
);
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
if (isLeadIn())
@@ -525,28 +550,40 @@ public class Game extends BasicGameState {
if (isReplay || GameMod.AUTO.isActive())
playbackSpeed.getButton().draw();
// draw music position bar (for replay seeking)
if (isReplay && Options.isReplaySeekingEnabled()) {
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? MUSICBAR_HOVER : MUSICBAR_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
if (!isLeadIn()) {
g.setColor(MUSICBAR_FILL);
float musicBarPosition = Math.min((float) trackPosition / beatmap.endTime, 1f);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight * musicBarPosition, 4);
}
}
// returning from pause screen
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
if (pauseTime > -1 && pausedMousePosition != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
// draw glowing hit select circle and pulse effect
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
int circleDiameter = GameImage.HITCIRCLE.getImage().getWidth();
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleDiameter, circleDiameter);
cursorCircle.setAlpha(1.0f);
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
cursorCircle.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
cursorCirclePulse.setAlpha(1f - pausePulse);
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
cursorCirclePulse.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
}
if (isReplay)
UI.draw(g, replayX, replayY, replayKeyPressed);
else if (GameMod.AUTO.isActive())
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, autoMousePressed);
else if (GameMod.AUTOPILOT.isActive())
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, Utils.isGameKeyPressed());
else
UI.draw(g);
}
@@ -564,8 +601,7 @@ public class Game extends BasicGameState {
// returning from pause screen: must click previous mouse position
if (pauseTime > -1) {
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
if ((pausedMouseX < 0 && pausedMouseY < 0) ||
(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
if (pausedMousePosition == null || (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
pauseTime = -1;
if (!isLeadIn())
MusicController.resume();
@@ -603,6 +639,17 @@ public class Game extends BasicGameState {
return;
}
// "Easy" mod: multiple "lives"
if (GameMod.EASY.isActive() && deathTime > -1) {
if (data.getHealth() < 99f) {
data.changeHealth(delta / 10f);
data.updateDisplays(delta);
return;
}
MusicController.resume();
deathTime = -1;
}
// normal game update
if (!isReplay)
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
@@ -613,7 +660,7 @@ public class Game extends BasicGameState {
if (replayIndex >= replay.frames.length)
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
//TODO probably should to disable sounds then reseek to the new position
// seeking to a position earlier than original track position
if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length &&
trackPosition < replay.frames[replayIndex - 1].getTime()) {
replayIndex = 0;
@@ -633,7 +680,6 @@ public class Game extends BasicGameState {
timingPointIndex++;
}
}
isSeeking = false;
}
// update and run replay frames
@@ -648,6 +694,12 @@ public class Game extends BasicGameState {
}
mouseX = replayX;
mouseY = replayY;
// unmute sounds
if (isSeeking) {
isSeeking = false;
SoundController.mute(false);
}
}
data.updateDisplays(delta);
@@ -662,16 +714,6 @@ public class Game extends BasicGameState {
* @param keys the keys that are pressed
*/
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
// "Easy" mod: multiple "lives"
if (GameMod.EASY.isActive() && deathTime > -1) {
if (data.getHealth() < 99f)
data.changeHealth(delta / 10f);
else {
MusicController.resume();
deathTime = -1;
}
}
// map complete!
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result
@@ -699,6 +741,7 @@ public class Game extends BasicGameState {
r.save();
}
ScoreData score = data.getScoreData(beatmap);
data.setGameplay(!isReplay);
// add score to database
if (!unranked && !isReplay)
@@ -745,8 +788,7 @@ public class Game extends BasicGameState {
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -819,8 +861,7 @@ public class Game extends BasicGameState {
// pause game
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -888,6 +929,13 @@ public class Game extends BasicGameState {
}
}
break;
case Input.KEY_F:
// change playback speed
if (isReplay || GameMod.AUTO.isActive()) {
playbackSpeed = playbackSpeed.next();
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
}
break;
case Input.KEY_UP:
UI.changeVolume(1);
break;
@@ -923,9 +971,10 @@ public class Game extends BasicGameState {
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
}
// TODO
else if (!GameMod.AUTO.isActive() && y < 50) {
float pos = (float) x / container.getWidth() * beatmap.endTime;
// replay seeking
else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPositionBarContains(x, y)) {
SoundController.mute(true); // mute sounds while seeking
float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime;
MusicController.setPosition((int) pos);
isSeeking = true;
}
@@ -939,8 +988,7 @@ public class Game extends BasicGameState {
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition();
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = x;
pausedMouseY = y;
pausedMousePosition = new Vec2f(x, y);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -969,13 +1017,12 @@ public class Game extends BasicGameState {
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
// returning from pause screen
if (pauseTime > -1) {
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
double distance = Math.hypot(pausedMousePosition.x - x, pausedMousePosition.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
// unpause the game
pauseTime = -1;
pausedMouseX = -1;
pausedMouseY = -1;
pausedMousePosition = null;
if (!isLeadIn())
MusicController.resume();
}
@@ -1065,6 +1112,15 @@ public class Game extends BasicGameState {
// restart the game
if (restart != Restart.FALSE) {
// load mods
if (isReplay) {
previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods);
}
data.setGameplay(true);
// check restart state
if (restart == Restart.NEW) {
// new game
loadImages();
@@ -1149,10 +1205,6 @@ public class Game extends BasicGameState {
// load replay frames
if (isReplay) {
// load mods
previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods);
// load initial data
replayX = container.getWidth() / 2;
replayY = container.getHeight() / 2;
@@ -1188,6 +1240,8 @@ public class Game extends BasicGameState {
MusicController.setPosition(0);
MusicController.setPitch(GameMod.getSpeedMultiplier());
MusicController.pause();
SoundController.mute(false);
}
skipButton.resetHover();
@@ -1242,10 +1296,10 @@ public class Game extends BasicGameState {
final int followPointInterval = container.getHeight() / 14;
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
int objectStartTime = beatmap.objects[index].getTime();
float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
float[] endXY = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endXY[0] - startXY[0];
float yDiff = endXY[1] - startXY[1];
Vec2f startPoint = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
Vec2f endPoint = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endPoint.x - startPoint.x;
float yDiff = endPoint.y - startPoint.y;
float dist = (float) Math.hypot(xDiff, yDiff);
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
if (numPoints > 0) {
@@ -1266,8 +1320,8 @@ public class Game extends BasicGameState {
float step = 1f / (numPoints + 1);
float t = step;
for (int i = 0; i < numPoints; i++) {
float x = startXY[0] + xDiff * t;
float y = startXY[1] + yDiff * t;
float x = startPoint.x + xDiff * t;
float y = startPoint.y + yDiff * t;
float nextT = t + step;
if (lastObjectIndex < objectIndex) { // fade the previous trail
if (progress < nextT) {
@@ -1321,8 +1375,7 @@ public class Game extends BasicGameState {
timingPointIndex = 0;
beatLengthBase = beatLength = 1;
pauseTime = -1;
pausedMouseX = -1;
pausedMouseY = -1;
pausedMousePosition = null;
countdownReadySound = false;
countdown3Sound = false;
countdown1Sound = false;
@@ -1333,8 +1386,7 @@ public class Game extends BasicGameState {
deathTime = -1;
replayFrames = null;
lastReplayTime = 0;
autoMouseX = 0;
autoMouseY = 0;
autoMousePosition = new Vec2f();
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
@@ -1376,9 +1428,9 @@ public class Game extends BasicGameState {
// set images
File parent = beatmap.getFile().getParentFile();
for (GameImage img : GameImage.values()) {
if (img.isSkinnable()) {
if (img.isBeatmapSkinnable()) {
img.setDefaultImage();
img.setSkinImage(parent);
img.setBeatmapSkinImage(parent);
}
}
@@ -1390,6 +1442,8 @@ public class Game extends BasicGameState {
Image skip = GameImage.SKIP.getImage();
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
}
skipButton.setHoverAnimationDuration(350);
skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT);
// load other images...
@@ -1420,14 +1474,15 @@ public class Game extends BasicGameState {
// Stack modifier scales with hit object size
// StackOffset = HitObjectRadius / 10
int diameter = (int) (104 - (circleSize * 8));
//int diameter = (int) (104 - (circleSize * 8));
float diameter = 108.848f - (circleSize * 8.9646f);
HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
// initialize objects
Circle.init(container, circleSize);
Slider.init(container, circleSize, beatmap);
Circle.init(container, diameter);
Slider.init(container, diameter, beatmap);
Spinner.init(container, overallDifficulty);
Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
Curve.init(container.getWidth(), container.getHeight(), diameter, (Options.isBeatmapSkinIgnored()) ?
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
// approachRate (hit object approach time)
@@ -1438,9 +1493,9 @@ public class Game extends BasicGameState {
// overallDifficulty (hit result time offsets)
hitResultOffset = new int[GameData.HIT_MAX];
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
//final float mult = 0.608f;
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
@@ -1454,6 +1509,14 @@ public class Game extends BasicGameState {
// difficulty multiplier (scoring)
data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);
// hit object fade-in time (TODO: formula)
fadeInTime = Math.min(375, (int) (approachTime / 2.5f));
// fade times ("Hidden" mod)
// TODO: find the actual formulas for this
hiddenDecayTime = (int) (approachTime / 3.6f);
hiddenTimeDiff = (int) (approachTime / 3.3f);
}
/**
@@ -1477,6 +1540,22 @@ public class Game extends BasicGameState {
*/
public int getApproachTime() { return approachTime; }
/**
* Returns the amount of time for hit objects to fade in, in milliseconds.
*/
public int getFadeInTime() { return fadeInTime; }
/**
* Returns the object decay time in the "Hidden" mod, in milliseconds.
*/
public int getHiddenDecayTime() { return hiddenDecayTime; }
/**
* Returns the time before the hit object time by which the objects have
* completely faded in the "Hidden" mod, in milliseconds.
*/
public int getHiddenTimeDiff() { return hiddenTimeDiff; }
/**
* Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants).
*/
@@ -1536,8 +1615,8 @@ public class Game extends BasicGameState {
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
// "auto" and "autopilot" mods: use automatic cursor coordinates
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
x = autoMouseX;
y = autoMouseY;
x = (int) autoMousePosition.x;
y = (int) autoMousePosition.y;
}
ReplayFrame frame = addReplayFrame(x, y, keys, time);
@@ -1611,17 +1690,13 @@ public class Game extends BasicGameState {
* @param endX the ending x coordinate
* @param endY the ending y coordinate
* @param t the t value [0, 1]
* @return the [x,y] coordinates
* @return the position vector
*/
private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
private Vec2f getPointAt(float startX, float startY, float endX, float endY, float t) {
// "autopilot" mod: move quicker between objects
if (GameMod.AUTOPILOT.isActive())
t = Utils.clamp(t * 2f, 0f, 1f);
float[] xy = new float[2];
xy[0] = startX + (endX - startX) * t;
xy[1] = startY + (endY - startY) * t;
return xy;
return new Vec2f(startX + (endX - startX) * t, startY + (endY - startY) * t);
}
/**
@@ -1715,9 +1790,9 @@ public class Game extends BasicGameState {
// possible special case: if slider end in the stack,
// all next hit objects in stack move right down
if (hitObjectN.isSlider()) {
float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
Vec2f p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
Vec2f p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// check if hit object part of this stack
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
@@ -1725,7 +1800,7 @@ public class Game extends BasicGameState {
for (int j = n + 1; j <= i; j++) {
HitObject hitObjectJ = beatmap.objects[j];
p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// hit object below slider end
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
@@ -1753,4 +1828,14 @@ public class Game extends BasicGameState {
gameObjects[i].updatePosition();
}
}
/**
* Returns true if the coordinates are within the music position bar bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
private boolean musicPositionBarContains(float cx, float cy) {
return ((cx > musicBarX && cx < musicBarX + musicBarWidth) &&
(cy > musicBarY && cy < musicBarY + musicBarHeight));
}
}

View File

@@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.lwjgl.input.Keyboard;
import org.newdawn.slick.Color;
@@ -61,7 +62,7 @@ public class GamePauseMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
private Game gameState;
public GamePauseMenu(int state) {
@@ -86,10 +87,10 @@ public class GamePauseMenu extends BasicGameState {
// don't draw default background if button skinned and background unskinned
boolean buttonsSkinned =
GameImage.PAUSE_CONTINUE.hasSkinImage() ||
GameImage.PAUSE_RETRY.hasSkinImage() ||
GameImage.PAUSE_BACK.hasSkinImage();
if (!buttonsSkinned || bg.hasSkinImage())
GameImage.PAUSE_CONTINUE.hasBeatmapSkinImage() ||
GameImage.PAUSE_RETRY.hasBeatmapSkinImage() ||
GameImage.PAUSE_BACK.hasBeatmapSkinImage();
if (!buttonsSkinned || bg.hasBeatmapSkinImage())
bg.getImage().draw();
else
g.setBackground(Color.black);
@@ -133,7 +134,7 @@ public class GamePauseMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else {
@@ -187,7 +188,7 @@ public class GamePauseMenu extends BasicGameState {
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
else
MusicController.resume();
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
@@ -227,6 +228,14 @@ public class GamePauseMenu extends BasicGameState {
continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f);
retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f);
backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f);
final int buttonAnimationDuration = 300;
continueButton.setHoverAnimationDuration(buttonAnimationDuration);
retryButton.setHoverAnimationDuration(buttonAnimationDuration);
backButton.setHoverAnimationDuration(buttonAnimationDuration);
final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK;
continueButton.setHoverAnimationEquation(buttonAnimationEquation);
retryButton.setHoverAnimationEquation(buttonAnimationEquation);
backButton.setHoverAnimationEquation(buttonAnimationEquation);
continueButton.setHoverExpand();
retryButton.setHoverExpand();
backButton.setHoverExpand();

View File

@@ -20,6 +20,7 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
@@ -68,7 +69,7 @@ public class GameRanking extends BasicGameState {
// game-related variables
private GameContainer container;
private StateBasedGame game;
private int state;
private final int state;
private Input input;
public GameRanking(int state) {
@@ -105,7 +106,7 @@ public class GameRanking extends BasicGameState {
Beatmap beatmap = MusicController.getBeatmap();
// background
if (!beatmap.drawBG(width, height, 0.7f, true))
if (!beatmap.drawBackground(width, height, 0.7f, true))
GameImage.PLAYFIELD.getImage().draw(0,0);
// ranking screen elements
@@ -113,7 +114,7 @@ public class GameRanking extends BasicGameState {
// buttons
replayButton.draw();
if (data.isGameplay())
if (data.isGameplay() && !GameMod.AUTO.isActive())
retryButton.draw();
UI.getBackButton().draw();
@@ -175,7 +176,8 @@ public class GameRanking extends BasicGameState {
// replay
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
boolean returnToGame = false;
if (replayButton.contains(x, y)) {
boolean replayButtonPressed = replayButton.contains(x, y);
if (replayButtonPressed && !(data.isGameplay() && GameMod.AUTO.isActive())) {
Replay r = data.getReplay(null, null);
if (r != null) {
try {
@@ -194,7 +196,9 @@ public class GameRanking extends BasicGameState {
}
// retry
else if (data.isGameplay() && retryButton.contains(x, y)) {
else if (data.isGameplay() &&
(!GameMod.AUTO.isActive() && retryButton.contains(x, y)) ||
(GameMod.AUTO.isActive() && replayButtonPressed)) {
gameState.setReplay(null);
gameState.setRestart(Game.Restart.MANUAL);
returnToGame = true;
@@ -221,7 +225,7 @@ public class GameRanking extends BasicGameState {
} else {
SoundController.playSound(SoundEffect.APPLAUSE);
retryButton.resetHover();
replayButton.setY(replayY);
replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY);
}
replayButton.resetHover();
}
@@ -239,12 +243,11 @@ public class GameRanking extends BasicGameState {
*/
private void returnToSongMenu() {
SoundController.playSound(SoundEffect.MENUBACK);
if (data.isGameplay()) {
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
songMenu.resetGameDataOnLoad();
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
if (data.isGameplay())
songMenu.resetTrackOnLoad();
}
if (UI.getCursor().isSkinned())
songMenu.resetGameDataOnLoad();
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}

View File

@@ -31,9 +31,13 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.MenuButton.Expand;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Desktop;
import java.io.IOException;
@@ -61,7 +65,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition;
*/
public class MainMenu extends BasicGameState {
/** Idle time, in milliseconds, before returning the logo to its original position. */
private static final short MOVE_DELAY = 5000;
private static final short LOGO_IDLE_DELAY = 10000;
/** Max alpha level of the menu background. */
private static final float BG_MAX_ALPHA = 0.9f;
@@ -69,12 +73,21 @@ public class MainMenu extends BasicGameState {
/** Logo button that reveals other buttons on click. */
private MenuButton logo;
/** Whether or not the logo has been clicked. */
private boolean logoClicked = false;
/** Logo states. */
private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING }
/** Current logo state. */
private LogoState logoState = LogoState.DEFAULT;
/** Delay timer, in milliseconds, before starting to move the logo back to the center. */
private int logoTimer = 0;
/** Logo horizontal offset for opening and closing actions. */
private AnimatedValue logoOpen, logoClose;
/** Logo button alpha levels. */
private AnimatedValue logoButtonAlpha;
/** Main "Play" and "Exit" buttons. */
private MenuButton playButton, exitButton;
@@ -87,8 +100,8 @@ public class MainMenu extends BasicGameState {
/** Button linking to repository. */
private MenuButton repoButton;
/** Button for installing updates. */
private MenuButton updateButton;
/** Buttons for installing updates. */
private MenuButton updateButton, restartButton;
/** Application start time, for drawing the total running time. */
private long programStartTime;
@@ -97,7 +110,7 @@ public class MainMenu extends BasicGameState {
private Stack<Integer> previous;
/** Background alpha level (for fade-in effect). */
private float bgAlpha = 0f;
private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR);
/** Whether or not a notification was already sent upon entering. */
private boolean enterNotification = false;
@@ -105,16 +118,11 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Music position bar background colors. */
private static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f);
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public MainMenu(int state) {
this.state = state;
@@ -145,9 +153,18 @@ public class MainMenu extends BasicGameState {
exitButton = new MenuButton(exitImg,
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
);
logo.setHoverExpand(1.05f);
playButton.setHoverExpand(1.05f);
exitButton.setHoverExpand(1.05f);
final int logoAnimationDuration = 350;
logo.setHoverAnimationDuration(logoAnimationDuration);
playButton.setHoverAnimationDuration(logoAnimationDuration);
exitButton.setHoverAnimationDuration(logoAnimationDuration);
final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK;
logo.setHoverAnimationEquation(logoAnimationEquation);
playButton.setHoverAnimationEquation(logoAnimationEquation);
exitButton.setHoverAnimationEquation(logoAnimationEquation);
final float logoHoverScale = 1.08f;
logo.setHoverExpand(logoHoverScale);
playButton.setHoverExpand(logoHoverScale);
exitButton.setHoverExpand(logoHoverScale);
// initialize music buttons
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
@@ -170,24 +187,40 @@ public class MainMenu extends BasicGameState {
// initialize downloads button
Image dlImg = GameImage.DOWNLOADS.getImage();
downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f);
downloadsButton.setHoverAnimationDuration(350);
downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
downloadsButton.setHoverExpand(1.03f, Expand.LEFT);
// initialize repository button
float startX = width * 0.997f, startY = height * 0.997f;
if (Desktop.isDesktopSupported()) { // only if a webpage can be opened
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
Image repoImg = GameImage.REPOSITORY.getImage();
repoButton = new MenuButton(repoImg,
startX - repoImg.getWidth(), startY - repoImg.getHeight()
);
repoButton.setHoverAnimationDuration(350);
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
repoButton.setHoverExpand();
startX -= repoImg.getWidth() * 1.75f;
} else
startX -= width * 0.005f;
}
// initialize update button
Image bangImg = GameImage.BANG.getImage();
updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight());
updateButton.setHoverExpand(1.15f);
// initialize update buttons
float updateX = width / 2f, updateY = height * 17 / 18f;
Image downloadImg = GameImage.DOWNLOAD.getImage();
updateButton = new MenuButton(downloadImg, updateX, updateY);
updateButton.setHoverAnimationDuration(400);
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
updateButton.setHoverExpand(1.1f);
Image updateImg = GameImage.UPDATE.getImage();
restartButton = new MenuButton(updateImg, updateX, updateY);
restartButton.setHoverAnimationDuration(2000);
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
restartButton.setHoverRotate(360);
// logo animations
float centerOffsetX = width / 5f;
logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD);
logoButtonAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
reset();
}
@@ -201,27 +234,27 @@ public class MainMenu extends BasicGameState {
// draw background
Beatmap beatmap = MusicController.getBeatmap();
if (Options.isDynamicBackgroundEnabled() &&
beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true))
;
else {
Image bg = GameImage.MENU_BG.getImage();
bg.setAlpha(bgAlpha);
bg.setAlpha(bgAlpha.getValue());
bg.draw();
}
// top/bottom horizontal bars
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = 0.2f;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlpha = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = 0.2f;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height / 9f);
g.fillRect(0, height * 8 / 9f, width, height / 9f);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Colors.BLACK_ALPHA.a = oldAlpha;
// draw downloads button
downloadsButton.draw();
// draw buttons
if (logoTimer > 0) {
if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) {
playButton.draw();
exitButton.draw();
}
@@ -237,7 +270,7 @@ public class MainMenu extends BasicGameState {
// draw music position bar
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white);
if (!MusicController.isTrackLoading() && beatmap != null) {
@@ -251,35 +284,26 @@ public class MainMenu extends BasicGameState {
// draw update button
if (Updater.get().showButton()) {
Color updateColor = null;
switch (Updater.get().getStatus()) {
case UPDATE_AVAILABLE:
updateColor = Color.red;
break;
case UPDATE_DOWNLOADED:
updateColor = Color.green;
break;
case UPDATE_DOWNLOADING:
updateColor = Color.yellow;
break;
default:
updateColor = Color.white;
break;
}
updateButton.draw(updateColor);
Updater.Status status = Updater.get().getStatus();
if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING)
updateButton.draw();
else if (status == Updater.Status.UPDATE_DOWNLOADED)
restartButton.draw();
}
// draw text
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
g.setFont(Utils.FONT_MEDIUM);
float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
g.setFont(Fonts.MEDIUM);
float lineHeight = Fonts.MEDIUM.getLineHeight() * 0.925f;
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
if (MusicController.isTrackLoading())
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
else if (MusicController.trackExists()) {
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
if (Options.useUnicodeMetadata()) { // load glyphs
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.artistUnicode);
}
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
g.drawString(String.format("%s: %s", beatmap.getArtist(), beatmap.getTitle()), marginX + 25, topMarginY + (lineHeight * 2));
}
@@ -305,7 +329,10 @@ public class MainMenu extends BasicGameState {
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
if (repoButton != null)
repoButton.hoverUpdate(delta, mouseX, mouseY);
updateButton.hoverUpdate(delta, mouseX, mouseY);
if (Updater.get().showButton()) {
updateButton.autoHoverUpdate(delta, true);
restartButton.autoHoverUpdate(delta, false);
}
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
// ensure only one button is in hover state at once
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
@@ -322,46 +349,46 @@ public class MainMenu extends BasicGameState {
MusicController.toggleTrackDimmed(0.33f);
// fade in background
if (bgAlpha < BG_MAX_ALPHA) {
bgAlpha += delta / 1000f;
if (bgAlpha > BG_MAX_ALPHA)
bgAlpha = BG_MAX_ALPHA;
}
Beatmap beatmap = MusicController.getBeatmap();
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
bgAlpha.update(delta);
// buttons
if (logoClicked) {
if (logoTimer == 0) { // shifting to left
if (logo.getX() > container.getWidth() / 3.3f)
logo.setX(logo.getX() - delta);
else
logoTimer = 1;
} else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center
logoClicked = false;
else { // increment timer
int centerX = container.getWidth() / 2;
float currentLogoButtonAlpha;
switch (logoState) {
case DEFAULT:
break;
case OPENING:
if (logoOpen.update(delta)) // shifting to left
logo.setX(centerX - logoOpen.getValue());
else {
logoState = LogoState.OPEN;
logoTimer = 0;
logoButtonAlpha.setTime(0);
}
break;
case OPEN:
if (logoButtonAlpha.update(delta)) { // fade in buttons
currentLogoButtonAlpha = logoButtonAlpha.getValue();
playButton.getImage().setAlpha(currentLogoButtonAlpha);
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
} else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center
logoState = LogoState.CLOSING;
logoClose.setTime(0);
logoTimer = 0;
} else // increment timer
logoTimer += delta;
if (logoTimer <= 500) {
// fade in buttons
playButton.getImage().setAlpha(logoTimer / 400f);
exitButton.getImage().setAlpha(logoTimer / 400f);
}
}
} else {
// fade out buttons
if (logoTimer > 0) {
float alpha = playButton.getImage().getAlpha();
if (alpha > 0f) {
playButton.getImage().setAlpha(alpha - (delta / 200f));
exitButton.getImage().setAlpha(alpha - (delta / 200f));
} else
logoTimer = 0;
}
// move back to original location
if (logo.getX() < container.getWidth() / 2) {
logo.setX(logo.getX() + (delta / 3f));
if (logo.getX() > container.getWidth() / 2)
logo.setX(container.getWidth() / 2);
break;
case CLOSING:
if (logoButtonAlpha.update(-delta)) { // fade out buttons
currentLogoButtonAlpha = logoButtonAlpha.getValue();
playButton.getImage().setAlpha(currentLogoButtonAlpha);
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
}
if (logoClose.update(delta)) // shifting to right
logo.setX(centerX - logoClose.getValue());
break;
}
// tooltips
@@ -373,8 +400,12 @@ public class MainMenu extends BasicGameState {
UI.updateTooltip(delta, "Next track", false);
else if (musicPrevious.contains(mouseX, mouseY))
UI.updateTooltip(delta, "Previous track", false);
else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY))
UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true);
else if (Updater.get().showButton()) {
Updater.Status status = Updater.get().getStatus();
if (((status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) && updateButton.contains(mouseX, mouseY)) ||
(status == Updater.Status.UPDATE_DOWNLOADED && restartButton.contains(mouseX, mouseY)))
UI.updateTooltip(delta, status.getDescription(), true);
}
}
@Override
@@ -412,8 +443,8 @@ public class MainMenu extends BasicGameState {
musicPrevious.resetHover();
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
repoButton.resetHover();
if (!updateButton.contains(mouseX, mouseY))
updateButton.resetHover();
updateButton.resetHover();
restartButton.resetHover();
if (!downloadsButton.contains(mouseX, mouseY))
downloadsButton.resetHover();
}
@@ -449,71 +480,85 @@ public class MainMenu extends BasicGameState {
MusicController.resume();
UI.sendBarNotification("Play");
}
return;
} else if (musicNext.contains(x, y)) {
nextTrack();
UI.sendBarNotification(">> Next");
return;
} else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
if (Options.isDynamicBackgroundEnabled())
bgAlpha = 0f;
bgAlpha.setTime(0);
} else
MusicController.setPosition(0);
UI.sendBarNotification("<< Previous");
return;
}
// downloads button actions
else if (downloadsButton.contains(x, y)) {
if (downloadsButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return;
}
// repository button actions
else if (repoButton != null && repoButton.contains(x, y)) {
if (repoButton != null && repoButton.contains(x, y)) {
try {
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
} catch (UnsupportedOperationException e) {
UI.sendBarNotification("The repository web page could not be opened.");
} catch (IOException e) {
ErrorHandler.error("Could not browse to repository URI.", e, false);
}
return;
}
// update button actions
else if (Updater.get().showButton() && updateButton.contains(x, y)) {
switch (Updater.get().getStatus()) {
case UPDATE_AVAILABLE:
if (Updater.get().showButton()) {
Updater.Status status = Updater.get().getStatus();
if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().startDownload();
break;
case UPDATE_DOWNLOADED:
updateButton.removeHoverEffects();
updateButton.setHoverAnimationDuration(800);
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
updateButton.setHoverFade(0.6f);
return;
} else if (restartButton.contains(x, y) && status == Updater.Status.UPDATE_DOWNLOADED) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().prepareUpdate();
container.setForceExit(false);
container.exit();
break;
default:
break;
return;
}
}
// start moving logo (if clicked)
else if (!logoClicked) {
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
if (logo.contains(x, y, 0.25f)) {
logoClicked = true;
logoState = LogoState.OPENING;
logoOpen.setTime(0);
logoTimer = 0;
playButton.getImage().setAlpha(0f);
exitButton.getImage().setAlpha(0f);
SoundController.playSound(SoundEffect.MENUHIT);
return;
}
}
// other button actions (if visible)
else if (logoClicked) {
else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) {
if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) {
SoundController.playSound(SoundEffect.MENUHIT);
enterSongMenu();
} else if (exitButton.contains(x, y, 0.25f))
return;
} else if (exitButton.contains(x, y, 0.25f)) {
container.exit();
return;
}
}
}
@@ -532,8 +577,9 @@ public class MainMenu extends BasicGameState {
break;
case Input.KEY_P:
SoundController.playSound(SoundEffect.MENUHIT);
if (!logoClicked) {
logoClicked = true;
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
logoState = LogoState.OPENING;
logoOpen.setTime(0);
logoTimer = 0;
playButton.getImage().setAlpha(0f);
exitButton.getImage().setAlpha(0f);
@@ -581,8 +627,11 @@ public class MainMenu extends BasicGameState {
public void reset() {
// reset logo
logo.setX(container.getWidth() / 2);
logoClicked = false;
logoOpen.setTime(0);
logoClose.setTime(0);
logoButtonAlpha.setTime(0);
logoTimer = 0;
logoState = LogoState.DEFAULT;
logo.resetHover();
playButton.resetHover();
@@ -594,6 +643,7 @@ public class MainMenu extends BasicGameState {
if (repoButton != null)
repoButton.resetHover();
updateButton.resetHover();
restartButton.resetHover();
downloadsButton.resetHover();
}
@@ -611,7 +661,7 @@ public class MainMenu extends BasicGameState {
previous.add(node.index);
}
if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying())
bgAlpha = 0f;
bgAlpha.setTime(0);
}
/**

View File

@@ -26,6 +26,8 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -86,14 +88,18 @@ public class OptionsMenu extends BasicGameState {
GameOption.DISABLE_MOUSE_WHEEL,
GameOption.DISABLE_MOUSE_BUTTONS,
GameOption.CURSOR_SIZE,
GameOption.NEW_CURSOR
GameOption.NEW_CURSOR,
GameOption.DISABLE_CURSOR
}),
CUSTOM ("Custom", new GameOption[] {
GameOption.FIXED_CS,
GameOption.FIXED_HP,
GameOption.FIXED_AR,
GameOption.FIXED_OD,
GameOption.CHECKPOINT
GameOption.CHECKPOINT,
GameOption.REPLAY_SEEKING,
GameOption.DISABLE_UPDATER,
GameOption.ENABLE_WATCH_SERVICE
});
/** Total number of tabs. */
@@ -110,10 +116,10 @@ public class OptionsMenu extends BasicGameState {
private static OptionTab[] values = values();
/** Tab name. */
private String name;
private final String name;
/** Options array. */
public GameOption[] options;
public final GameOption[] options;
/** Associated tab button. */
public MenuButton button;
@@ -163,7 +169,7 @@ public class OptionsMenu extends BasicGameState {
private StateBasedGame game;
private Input input;
private Graphics g;
private int state;
private final int state;
public OptionsMenu(int state) {
this.state = state;
@@ -182,8 +188,8 @@ public class OptionsMenu extends BasicGameState {
// option tabs
Image tabImage = GameImage.MENU_TAB.getImage();
float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
float tabX = width * 0.032f + Fonts.DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
float tabY = Fonts.XLARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
height * 0.015f - (tabImage.getHeight() / 2f);
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
for (OptionTab tab : OptionTab.values())
@@ -198,22 +204,19 @@ public class OptionsMenu extends BasicGameState {
@Override
public void render(GameContainer container, StateBasedGame game, Graphics g)
throws SlickException {
g.setBackground(Utils.COLOR_BLACK_ALPHA);
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
// background
GameImage.OPTIONS_BG.getImage().draw();
// title
float marginX = width * 0.015f, marginY = height * 0.01f;
Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
Fonts.XLARGE.drawString(marginX, marginY, "Options", Color.white);
Fonts.DEFAULT.drawString(marginX, marginY + Fonts.XLARGE.getLineHeight() * 0.92f,
"Change the way opsu! behaves", Color.white);
// background
GameImage.OPTIONS_BG.getImage().draw(0, lineY);
// game options
g.setLineWidth(1f);
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
@@ -241,6 +244,7 @@ public class OptionsMenu extends BasicGameState {
currentTab.getName(), true, false);
g.setColor(Color.white);
g.setLineWidth(2f);
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
g.drawLine(0, lineY, width, lineY);
g.resetLineWidth();
@@ -248,15 +252,15 @@ public class OptionsMenu extends BasicGameState {
// key entry state
if (keyEntryLeft || keyEntryRight) {
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
g.setColor(Color.white);
String prompt = (keyEntryLeft) ?
"Please press the new left-click key." :
"Please press the new right-click key.";
Utils.FONT_LARGE.drawString(
(width / 2) - (Utils.FONT_LARGE.getWidth(prompt) / 2),
(height / 2) - Utils.FONT_LARGE.getLineHeight(), prompt
Fonts.LARGE.drawString(
(width / 2) - (Fonts.LARGE.getWidth(prompt) / 2),
(height / 2) - Fonts.LARGE.getLineHeight(), prompt
);
}
@@ -413,14 +417,14 @@ public class OptionsMenu extends BasicGameState {
*/
private void drawOption(GameOption option, int pos, boolean focus) {
int width = container.getWidth();
int textHeight = Utils.FONT_LARGE.getLineHeight();
int textHeight = Fonts.LARGE.getLineHeight();
float y = textY + (pos * offsetY);
Color color = (focus) ? Color.cyan : Color.white;
Utils.FONT_LARGE.drawString(width / 30, y, option.getName(), color);
Utils.FONT_LARGE.drawString(width / 2, y, option.getValueString(), color);
Utils.FONT_SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
g.setColor(Utils.COLOR_WHITE_ALPHA);
Fonts.LARGE.drawString(width / 30, y, option.getName(), color);
Fonts.LARGE.drawString(width / 2, y, option.getValueString(), color);
Fonts.SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
g.setColor(Colors.WHITE_ALPHA);
g.drawLine(0, y + textHeight, width, y + textHeight);
}
@@ -433,7 +437,7 @@ public class OptionsMenu extends BasicGameState {
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
return null;
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
int index = (y - textY + Fonts.LARGE.getLineHeight()) / offsetY;
if (index >= currentTab.options.length)
return null;

View File

@@ -24,7 +24,6 @@ import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MultiClip;
@@ -32,18 +31,32 @@ import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.beatmap.BeatmapWatchService.BeatmapWatchServiceListener;
import itdelatrisu.opsu.beatmap.LRUCache;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.KinecticScrolling;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.StarStream;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent.Kind;
import java.util.Map;
import java.util.Stack;
@@ -145,8 +158,8 @@ public class SongMenu extends BasicGameState {
/** Button coordinate values. */
private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight;
/** Current x offset of song buttons for mouse hover, in pixels. */
private float hoverOffset = 0f;
/** Horizontal offset of song buttons for mouse hover, in pixels. */
private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART);
/** Current index of hovered song button. */
private BeatmapSetNode hoverIndex = null;
@@ -209,11 +222,52 @@ public class SongMenu extends BasicGameState {
/** The text length of the last string in the search TextField. */
private int lastSearchTextLength = -1;
/** Whether the song folder changed (notified via the watch service). */
private boolean songFolderChanged = false;
/** The last background image. */
private File lastBackgroundImage;
/** Background alpha level (for fade-in effect). */
private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD);
/** Timer for animations when a new song node is selected. */
private AnimatedValue songChangeTimer = new AnimatedValue(900, 0f, 1f, AnimationEquation.LINEAR);
/** Timer for the music icon animation when a new song node is selected. */
private AnimatedValue musicIconBounceTimer = new AnimatedValue(350, 0f, 1f, AnimationEquation.LINEAR);
/**
* Beatmaps whose difficulties were recently computed (if flag is non-null).
* Unless the Boolean flag is null, then upon removal, the beatmap's objects will
* be cleared (to be garbage collected). If the flag is true, also clear the
* beatmap's array fields (timing points, etc.).
*/
@SuppressWarnings("serial")
private LRUCache<Beatmap, Boolean> beatmapsCalculated = new LRUCache<Beatmap, Boolean>(12) {
@Override
public void eldestRemoved(Map.Entry<Beatmap, Boolean> eldest) {
Boolean b = eldest.getValue();
if (b != null) {
Beatmap beatmap = eldest.getKey();
beatmap.objects = null;
if (b) {
beatmap.timingPoints = null;
beatmap.breaks = null;
beatmap.combo = null;
}
}
}
};
/** The star stream. */
private StarStream starStream;
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public SongMenu(int state) {
this.state = state;
@@ -231,8 +285,8 @@ public class SongMenu extends BasicGameState {
// header/footer coordinates
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
Utils.FONT_SMALL.getLineHeight();
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
Fonts.SMALL.getLineHeight();
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
// initialize sorts
@@ -253,11 +307,11 @@ public class SongMenu extends BasicGameState {
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
// search
int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: "));
int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2);
int textFieldX = (int) (width * 0.7125f + Fonts.BOLD.getWidth("Search: "));
int textFieldY = (int) (headerY + Fonts.BOLD.getLineHeight() / 2);
search = new TextField(
container, Utils.FONT_BOLD, textFieldX, textFieldY,
(int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight()
container, Fonts.BOLD, textFieldX, textFieldY,
(int) (width * 0.99f) - textFieldX, Fonts.BOLD.getLineHeight()
);
search.setBackgroundColor(Color.transparent);
search.setBorderColor(Color.transparent);
@@ -287,6 +341,22 @@ public class SongMenu extends BasicGameState {
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
loader = new Animation(spr, 50);
// beatmap watch service listener
final StateBasedGame game_ = game;
BeatmapWatchService.addListener(new BeatmapWatchServiceListener() {
@Override
public void eventReceived(Kind<?> kind, Path child) {
if (!songFolderChanged && kind != StandardWatchEventKinds.ENTRY_MODIFY) {
songFolderChanged = true;
if (game_.getCurrentStateID() == Opsu.STATE_SONGMENU)
UI.sendBarNotification("Changes in Songs folder detected. Hit F5 to refresh.");
}
}
});
// star stream
starStream = new StarStream(width, height);
}
@Override
@@ -300,23 +370,30 @@ public class SongMenu extends BasicGameState {
// background
if (focusNode != null) {
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
GameImage.PLAYFIELD.getImage().draw();
}
// star stream
starStream.draw();
// song buttons
BeatmapSetNode node = startNode;
int starNodeOffsetoffset = 0;
if (node.prev != null) {
starNodeOffsetoffset = -1;
node = node.prev;
}
g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY));
for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
for (int i = startNodeOffset + starNodeOffsetoffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
// draw the node
float offset = (node == hoverIndex) ? hoverOffset : 0f;
float offset = (node == hoverIndex) ? hoverOffset.getValue() : 0f;
float ypos = buttonY + (i*buttonOffset) ;
float mid = height/2 - ypos - buttonOffset/2;
final float circleRadi = 1000 * GameImage.getUIscale();
//finds points along a very large circle
// x^2 = h^2 - y^2
float t = circleRadi*circleRadi - (mid*mid);
final float circleRadi = 700 * GameImage.getUIscale();
//finds points along a very large circle (x^2 = h^2 - y^2)
float t = circleRadi * circleRadi - (mid * mid);
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
ScoreData[] scores = getScoreDataForNode(node, false);
node.draw(buttonX - offset - xpos, ypos,
@@ -336,7 +413,7 @@ public class SongMenu extends BasicGameState {
MAX_SONG_BUTTONS * buttonOffset,
width, headerY + DIVIDER_LINE_WIDTH / 2,
0, MAX_SONG_BUTTONS * buttonOffset,
Utils.COLOR_BLACK_ALPHA, Color.white, true);
Colors.BLACK_ALPHA, Color.white, true);
}
}
@@ -345,14 +422,19 @@ public class SongMenu extends BasicGameState {
ScoreData.clipToDownloadArea(g);
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
int rank = startScore + i;
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS + 1);
float timerScale = 1f - (1 / 3f) * ((MAX_SCORE_BUTTONS - scoreButtons) / (float) (MAX_SCORE_BUTTONS - 1));
int duration = (int) (songChangeTimer.getDuration() * timerScale);
int segmentDuration = (int) ((2 / 3f) * songChangeTimer.getDuration());
int time = songChangeTimer.getTime();
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
if (rank >= focusScores.length)
break;
long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1;
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, ScoreData.buttonContains(mouseX, mouseY-offset, i));
float t = Utils.clamp((time - (i * (duration - segmentDuration) / scoreButtons)) / (float) segmentDuration, 0f, 1f);
boolean focus = (t >= 0.9999f && ScoreData.buttonContains(mouseX, mouseY - offset, i));
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, focus, t);
}
g.clearClip();
@@ -360,13 +442,12 @@ public class SongMenu extends BasicGameState {
if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY))
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
}
// top/bottom bars
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, headerY);
g.fillRect(0, footerY, width, height - footerY);
g.setColor(Utils.COLOR_BLUE_DIVIDER);
g.setColor(Colors.BLUE_DIVIDER);
g.setLineWidth(DIVIDER_LINE_WIDTH);
g.drawLine(0, headerY, width, headerY);
g.drawLine(0, footerY, width, footerY);
@@ -379,8 +460,13 @@ public class SongMenu extends BasicGameState {
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
if (MusicController.isTrackLoading())
loader.draw(marginX, marginY);
else
musicNote.draw(marginX, marginY);
else {
float t = musicIconBounceTimer.getValue() * 2f;
if (t > 1)
t = 2f - t;
float musicNoteScale = 1f + 0.3f * t;
musicNote.getScaledCopy(musicNoteScale).drawCentered(marginX + musicNote.getWidth() / 2f, marginY + musicNote.getHeight() / 2f);
}
int iconWidth = musicNote.getWidth();
// song info text
@@ -388,26 +474,49 @@ public class SongMenu extends BasicGameState {
songInfo = focusNode.getInfo();
if (Options.useUnicodeMetadata()) { // load glyphs
Beatmap beatmap = focusNode.getBeatmapSet().get(0);
Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
Fonts.loadGlyphs(Fonts.LARGE, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.LARGE, beatmap.artistUnicode);
}
}
marginX += 5;
Color c = Colors.WHITE_FADE;
float oldAlpha = c.a;
float t = AnimationEquation.OUT_QUAD.calc(songChangeTimer.getValue());
float headerTextY = marginY * 0.2f;
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
float speedModifier = GameMod.getSpeedMultiplier();
Color color2 = (speedModifier == 1f) ? Color.white :
(speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
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);
c.a = Math.min(t * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], c);
headerTextY += Fonts.LARGE.getLineHeight() - 6;
c.a = Math.min((t - 1f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], c);
headerTextY += Fonts.DEFAULT.getLineHeight() - 2;
c.a = Math.min((t - 2f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0) {
float speedModifier = GameMod.getSpeedMultiplier();
Color color2 = (speedModifier == 1f) ? c :
(speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
float oldAlpha2 = color2.a;
color2.a = c.a;
Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
color2.a = oldAlpha2;
}
headerTextY += Fonts.BOLD.getLineHeight() - 4;
c.a = Math.min((t - 3f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.DEFAULT.drawString(marginX, headerTextY, songInfo[3], c);
headerTextY += Fonts.DEFAULT.getLineHeight() - 4;
c.a = Math.min((t - 4f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0) {
float multiplier = GameMod.getDifficultyMultiplier();
Color color4 = (multiplier == 1f) ? c :
(multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
float oldAlpha4 = color4.a;
color4.a = c.a;
Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
color4.a = oldAlpha4;
}
c.a = oldAlpha;
}
// selection buttons
@@ -440,38 +549,41 @@ public class SongMenu extends BasicGameState {
int searchX = search.getX(), searchY = search.getY();
float searchBaseX = width * 0.7f;
float searchTextX = width * 0.7125f;
float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2;
float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f;
float searchRectHeight = Fonts.BOLD.getLineHeight() * 2;
float searchExtraHeight = Fonts.DEFAULT.getLineHeight() * 0.7f;
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
float oldAlpha = Colors.BLACK_ALPHA.a;
if (searchEmpty) {
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
Colors.BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
} else {
searchRectHeight += searchProgress * searchExtraHeight;
Utils.COLOR_BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
Colors.BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
}
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH);
Colors.BLACK_ALPHA.a = oldAlpha;
Fonts.BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH);
if (searchEmpty)
Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
Fonts.BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
else {
g.setColor(Color.white);
// TODO: why is this needed to correctly position the TextField?
search.setLocation(searchX - 3, searchY - 1);
search.render(container, g);
search.setLocation(searchX, searchY);
Utils.FONT_DEFAULT.drawString(searchTextX, searchY + Utils.FONT_BOLD.getLineHeight(),
Fonts.DEFAULT.drawString(searchTextX, searchY + Fonts.BOLD.getLineHeight(),
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
}
// reloading beatmaps
if (reloadThread != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -510,6 +622,21 @@ public class SongMenu extends BasicGameState {
}
}
if (focusNode != null) {
// fade in background
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
if (!focusNodeBeatmap.isBackgroundLoading())
bgAlpha.update(delta);
// song change timers
songChangeTimer.update(delta);
if (!MusicController.isTrackLoading())
musicIconBounceTimer.update(delta);
}
// star stream
starStream.update(delta);
// search
search.setFocus(true);
searchTimer += delta;
@@ -574,14 +701,10 @@ public class SongMenu extends BasicGameState {
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
if (node == hoverIndex) {
if (hoverOffset < MAX_HOVER_OFFSET) {
hoverOffset += delta / 3f;
if (hoverOffset > MAX_HOVER_OFFSET)
hoverOffset = MAX_HOVER_OFFSET;
}
hoverOffset.update(delta);
} else {
hoverIndex = node ;
hoverOffset = 0f;
hoverOffset.setTime(0);
}
isHover = true;
break;
@@ -589,21 +712,19 @@ public class SongMenu extends BasicGameState {
}
}
if (!isHover) {
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
} else
return;
// tooltips
if (focusScores != null) {
if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
for (int i = 0; i < MAX_SCORE_BUTTONS; i++) {
int rank = startScore + i;
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
if (rank >= focusScores.length)
break;
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
break;
@@ -689,7 +810,7 @@ public class SongMenu extends BasicGameState {
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
if ((x > cx && x < cx + buttonWidth) &&
(y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) {
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
// clicked node is already expanded
@@ -714,7 +835,7 @@ public class SongMenu extends BasicGameState {
}
// restore hover data
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
// open beatmap menu
@@ -728,12 +849,10 @@ public class SongMenu extends BasicGameState {
// score buttons
if (focusScores != null && ScoreData.areaContains(x, y)) {
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int rank = startScore + i;
if (rank >= focusScores.length)
break;
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (ScoreData.buttonContains(x, y - offset, i)) {
SoundController.playSound(SoundEffect.MENUHIT);
if (button != Input.MOUSE_RIGHT_BUTTON) {
@@ -805,8 +924,12 @@ public class SongMenu extends BasicGameState {
break;
case Input.KEY_F5:
SoundController.playSound(SoundEffect.MENUHIT);
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
game.enterState(Opsu.STATE_BUTTONMENU);
if (songFolderChanged)
reloadBeatmaps(false);
else {
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
game.enterState(Opsu.STATE_BUTTONMENU);
}
break;
case Input.KEY_DELETE:
if (focusNode == null)
@@ -851,11 +974,11 @@ public class SongMenu extends BasicGameState {
if (next != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
setFocus(next, 0, false, true);
if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
}
}
@@ -867,11 +990,11 @@ public class SongMenu extends BasicGameState {
if (prev != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
}
}
@@ -965,18 +1088,26 @@ public class SongMenu extends BasicGameState {
selectRandomButton.resetHover();
selectMapOptionsButton.resetHover();
selectOptionsButton.resetHover();
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
startScorePos.setPosition(0);
beatmapMenuTimer = -1;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
songInfo = null;
bgAlpha.setTime(bgAlpha.getDuration());
songChangeTimer.setTime(songChangeTimer.getDuration());
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
starStream.clear();
// reset song stack
randomStack = new Stack<SongNode>();
// reload beatmaps if song folder changed
if (songFolderChanged && stateAction != MenuState.RELOAD)
reloadBeatmaps(false);
// set focus node if not set (e.g. theme song playing)
if (focusNode == null && BeatmapSetList.get().size() > 0)
else if (focusNode == null && BeatmapSetList.get().size() > 0)
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
// reset music track
@@ -1003,13 +1134,13 @@ public class SongMenu extends BasicGameState {
// destroy skin images, if any
for (GameImage img : GameImage.values()) {
if (img.isSkinnable())
img.destroySkinImage();
if (img.isBeatmapSkinnable())
img.destroyBeatmapSkinImage();
}
// reload scores
if (focusNode != null) {
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
}
@@ -1022,7 +1153,7 @@ public class SongMenu extends BasicGameState {
case BEATMAP: // clear all scores
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
break;
Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
Beatmap beatmap = stateActionNode.getSelectedBeatmap();
ScoreDB.deleteScore(beatmap);
if (stateActionNode == focusNode) {
focusScores = null;
@@ -1033,7 +1164,7 @@ public class SongMenu extends BasicGameState {
if (stateActionScore == null)
break;
ScoreDB.deleteScore(stateActionScore);
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
startScorePos.setPosition(0);
break;
@@ -1095,44 +1226,7 @@ public class SongMenu extends BasicGameState {
}
break;
case RELOAD: // reload beatmaps
// reset state and node references
MusicController.reset();
startNode = focusNode = null;
scoreMap = null;
focusScores = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
hoverOffset = 0f;
hoverIndex = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
searchResultString = null;
// reload songs in new thread
reloadThread = new Thread() {
@Override
public void run() {
// clear the beatmap cache
BeatmapDB.clearDatabase();
// invoke unpacker and parser
File beatmapDir = Options.getBeatmapDir();
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
reloadThread = null;
}
};
reloadThread.start();
reloadBeatmaps(true);
break;
default:
break;
@@ -1210,15 +1304,25 @@ public class SongMenu extends BasicGameState {
if (node == null)
return null;
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
songInfo = null;
songChangeTimer.setTime(0);
musicIconBounceTimer.setTime(0);
BeatmapSetNode oldFocus = focusNode;
// expand node before focusing it
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index != expandedIndex) {
node = BeatmapSetList.get().expand(node.index);
// calculate difficulties
calculateStarRatings(node.getBeatmapSet());
// if start node was previously expanded, move it
if (startNode != null && startNode.index == expandedIndex)
startNode = BeatmapSetList.get().getBaseNode(startNode.index);
}
// check beatmapIndex bounds
@@ -1227,7 +1331,7 @@ public class SongMenu extends BasicGameState {
beatmapIndex = (int) (Math.random() * length);
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
Beatmap beatmap = focusNode.getSelectedBeatmap();
MusicController.play(beatmap, false, preview);
// load scores
@@ -1286,6 +1390,14 @@ public class SongMenu extends BasicGameState {
songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY));
//*/
// load background image
beatmap.loadBackground();
boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null;
if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) {
bgAlpha.setTime(0);
lastBackgroundImage = beatmap.bg;
}
return oldFocus;
}
@@ -1346,7 +1458,7 @@ public class SongMenu extends BasicGameState {
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
return null;
Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
Beatmap beatmap = node.getSelectedBeatmap();
ScoreData[] scores = scoreMap.get(beatmap.version);
if (scores == null || scores.length < 1) // no scores
return null;
@@ -1364,6 +1476,86 @@ public class SongMenu extends BasicGameState {
return null; // incorrect map
}
/**
* Reloads all beatmaps.
* @param fullReload if true, also clear the beatmap cache and invoke the unpacker
*/
private void reloadBeatmaps(final boolean fullReload) {
songFolderChanged = false;
// reset state and node references
MusicController.reset();
startNode = focusNode = null;
scoreMap = null;
focusScores = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
hoverOffset.setTime(0);
hoverIndex = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
searchResultString = null;
lastBackgroundImage = null;
// reload songs in new thread
reloadThread = new Thread() {
@Override
public void run() {
File beatmapDir = Options.getBeatmapDir();
if (fullReload) {
// clear the beatmap cache
BeatmapDB.clearDatabase();
// invoke unpacker
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
}
// invoke parser
BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
reloadThread = null;
}
};
reloadThread.start();
}
/**
* Calculates all star ratings for a beatmap set.
* @param beatmapSet the set of beatmaps
*/
private void calculateStarRatings(BeatmapSet beatmapSet) {
for (Beatmap beatmap : beatmapSet) {
if (beatmap.starRating >= 0) { // already calculated
beatmapsCalculated.put(beatmap, beatmapsCalculated.get(beatmap));
continue;
}
// if timing points are already loaded before this (for whatever reason),
// don't clear the array fields to be safe
boolean hasTimingPoints = (beatmap.timingPoints != null);
BeatmapDifficultyCalculator diffCalc = new BeatmapDifficultyCalculator(beatmap);
diffCalc.calculate();
if (diffCalc.getStarRating() == -1)
continue; // calculations failed
// save star rating
beatmap.starRating = diffCalc.getStarRating();
BeatmapDB.setStars(beatmap);
beatmapsCalculated.put(beatmap, !hasTimingPoints);
}
}
/**
* Starts the game.
*/
@@ -1372,8 +1564,12 @@ public class SongMenu extends BasicGameState {
return;
SoundController.playSound(SoundEffect.MENUHIT);
MultiClip.destroyExtraClips();
Beatmap beatmap = MusicController.getBeatmap();
if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
UI.sendBarNotification("Unable to load the beatmap audio.");
return;
}
MultiClip.destroyExtraClips();
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadBeatmap(beatmap);
gameState.setRestart(Game.Restart.NEW);

View File

@@ -21,21 +21,23 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
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.BasicGameState;
@@ -47,6 +49,9 @@ import org.newdawn.slick.state.StateBasedGame;
* Loads game resources and enters "Main Menu" state.
*/
public class Splash extends BasicGameState {
/** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */
private static final int MIN_SPLASH_TIME = 400;
/** Whether or not loading has completed. */
private boolean finished = false;
@@ -59,8 +64,14 @@ public class Splash extends BasicGameState {
/** Whether the skin being loaded is a new skin (for program restarts). */
private boolean newSkin = false;
/** Whether the watch service is newly enabled (for program restarts). */
private boolean watchServiceChange = false;
/** Logo alpha level. */
private AnimatedValue logoAlpha;
// game-related variables
private int state;
private final int state;
private GameContainer container;
private boolean init = false;
@@ -77,9 +88,14 @@ public class Splash extends BasicGameState {
if (Options.getSkin() != null)
this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
// check if watch service newly enabled
this.watchServiceChange = Options.isWatchServiceEnabled() && BeatmapWatchService.get() == null;
// load Utils class first (needed in other 'init' methods)
Utils.init(container, game);
// fade in logo
this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR);
GameImage.MENU_LOGO.getImage().setAlpha(0f);
}
@@ -99,13 +115,18 @@ public class Splash extends BasicGameState {
// resources already loaded (from application restart)
if (BeatmapSetList.get() != null) {
// reload sounds if skin changed
if (newSkin) {
if (newSkin || watchServiceChange) { // need to reload resources
thread = new Thread() {
@Override
public void run() {
// reload beatmaps if watch service newly enabled
if (watchServiceChange)
BeatmapParser.parseAllFiles(Options.getBeatmapDir());
// reload sounds if skin changed
// TODO: only reload each sound if actually needed?
SoundController.init();
if (newSkin)
SoundController.init();
finished = true;
thread = null;
@@ -144,13 +165,11 @@ public class Splash extends BasicGameState {
}
// fade in logo
Image logo = GameImage.MENU_LOGO.getImage();
float alpha = logo.getAlpha();
if (alpha < 1f)
logo.setAlpha(alpha + (delta / 500f));
if (logoAlpha.update(delta))
GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue());
// change states when loading complete
if (finished && alpha >= 1f) {
if (finished && logoAlpha.getValue() >= 1f) {
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();

View File

@@ -0,0 +1,51 @@
/*
* 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 org.newdawn.slick.Color;
/**
* Colors used for drawing.
*/
public class Colors {
public static final Color
BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
BLUE_DIVIDER = new Color(49, 94, 237),
BLUE_BACKGROUND = new Color(74, 130, 255),
BLUE_BUTTON = new Color(40, 129, 237),
ORANGE_BUTTON = new Color(200, 90, 3),
YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
WHITE_FADE = new Color(255, 255, 255, 1f),
RED_HOVER = new Color(255, 112, 112),
GREEN = new Color(137, 201, 79),
LIGHT_ORANGE = new Color(255, 192, 128),
LIGHT_GREEN = new Color(128, 255, 128),
LIGHT_BLUE = new Color(128, 128, 255),
GREEN_SEARCH = new Color(173, 255, 47),
DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
RED_HIGHLIGHT = new Color(246, 154, 161),
BLUE_HIGHLIGHT = new Color(173, 216, 230),
BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f),
BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f),
BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);
// This class should not be instantiated.
private Colors() {}
}

View File

@@ -24,9 +24,10 @@ import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Point;
import java.nio.IntBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import org.lwjgl.BufferUtils;
@@ -45,13 +46,25 @@ public class Cursor {
private static org.lwjgl.input.Cursor emptyCursor;
/** Last cursor coordinates. */
private int lastX = -1, lastY = -1;
private Point lastPosition;
/** Cursor rotation angle. */
private float cursorAngle = 0f;
/** The time in milliseconds when the cursor was last pressed, used for the scaling animation. */
private long lastCursorPressTime = 0L;
/** Whether or not the cursor was pressed in the last frame, used for the scaling animation. */
private boolean lastCursorPressState = false;
/** The amount the cursor scale increases, if enabled, when pressed. */
private static final float CURSOR_SCALE_CHANGE = 0.25f;
/** The time it takes for the cursor to scale, in milliseconds. */
private static final float CURSOR_SCALE_TIME = 125;
/** Stores all previous cursor locations to display a trail. */
private LinkedList<Integer> cursorX, cursorY;
private LinkedList<Point> trail = new LinkedList<Point>();
// game-related variables
private static GameContainer container;
@@ -81,10 +94,7 @@ public class Cursor {
/**
* Constructor.
*/
public Cursor() {
cursorX = new LinkedList<Integer>();
cursorY = new LinkedList<Integer>();
}
public Cursor() {}
/**
* Draws the cursor.
@@ -105,85 +115,90 @@ public class Cursor {
* @param mousePressed whether or not the mouse button is pressed
*/
public void draw(int mouseX, int mouseY, boolean mousePressed) {
if (Options.isCursorDisabled())
return;
// determine correct cursor image
Image cursor = null, cursorMiddle = null, cursorTrail = null;
boolean skinned = GameImage.CURSOR.hasSkinImage();
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
boolean newStyle, hasMiddle;
if (skinned) {
Skin skin = Options.getSkin();
if (beatmapSkinned) {
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
} else
newStyle = hasMiddle = Options.isNewCursorEnabled();
if (skinned || newStyle) {
if (newStyle || beatmapSkinned) {
cursor = GameImage.CURSOR.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
} else {
cursor = GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
}
if (hasMiddle)
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
int removeCount = 0;
int FPSmod = (Options.getTargetFPS() / 60);
Skin skin = Options.getSkin();
// scale cursor
float cursorScale = Options.getCursorScale();
if (mousePressed && skin.isCursorExpanded())
cursorScale *= 1.25f; // increase the cursor size if pressed
float cursorScaleAnimated = 1f;
if (skin.isCursorExpanded()) {
if (lastCursorPressState != mousePressed) {
lastCursorPressState = mousePressed;
lastCursorPressTime = System.currentTimeMillis();
}
float cursorScaleChange = CURSOR_SCALE_CHANGE * AnimationEquation.IN_OUT_CUBIC.calc(
Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME);
cursorScaleAnimated = 1f + ((mousePressed) ? cursorScaleChange : CURSOR_SCALE_CHANGE - cursorScaleChange);
}
float cursorScale = cursorScaleAnimated * Options.getCursorScale();
if (cursorScale != 1f) {
cursor = cursor.getScaledCopy(cursorScale);
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
if (hasMiddle)
cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
}
// TODO: use an image buffer
int removeCount = 0;
float FPSmod = Math.max(container.getFPS(), 1) / 60f;
if (newStyle) {
// new style: add all points between cursor movements
if (lastX < 0) {
lastX = mouseX;
lastY = mouseY;
if (lastPosition == null) {
lastPosition = new Point(mouseX, mouseY);
return;
}
addCursorPoints(lastX, lastY, mouseX, mouseY);
lastX = mouseX;
lastY = mouseY;
addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
lastPosition.move(mouseX, mouseY);
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
} else {
// old style: sample one point at a time
cursorX.add(mouseX);
cursorY.add(mouseY);
trail.add(new Point(mouseX, mouseY));
int max = 10 * FPSmod;
if (cursorX.size() > max)
removeCount = cursorX.size() - max;
int max = (int) (10 * FPSmod);
if (trail.size() > max)
removeCount = trail.size() - max;
}
// remove points from the lists
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
cursorX.remove();
cursorY.remove();
}
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
trail.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();
float t = 2f / trail.size();
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
cursorTrail.startUse();
for (Point p : trail) {
alpha += t;
cursorTrail.setAlpha(alpha);
// if (cx != x || cy != y)
cursorTrail.drawCentered(cx, cy);
cursorTrail.setImageColor(1f, 1f, 1f, alpha);
cursorTrail.drawEmbedded(
p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
}
cursorTrail.drawCentered(mouseX, mouseY);
cursorTrail.drawEmbedded(
mouseX - (cursorTrailWidth / 2f), mouseY - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
cursorTrail.endUse();
// draw the other components
if (newStyle && skin.isCursorRotated())
@@ -212,8 +227,7 @@ public class Cursor {
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (x1 == x2)
@@ -228,8 +242,7 @@ public class Cursor {
} else {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (y1 == y2)
@@ -255,13 +268,13 @@ public class Cursor {
}
/**
* Resets all cursor data and skins.
* Resets all cursor data and beatmap skins.
*/
public void reset() {
// destroy skin images
GameImage.CURSOR.destroySkinImage();
GameImage.CURSOR_MIDDLE.destroySkinImage();
GameImage.CURSOR_TRAIL.destroySkinImage();
GameImage.CURSOR.destroyBeatmapSkinImage();
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
// reset locations
resetLocations();
@@ -276,18 +289,17 @@ public class Cursor {
* Resets all cursor location data.
*/
public void resetLocations() {
lastX = lastY = -1;
cursorX.clear();
cursorY.clear();
lastPosition = null;
trail.clear();
}
/**
* Returns whether or not the cursor is skinned.
*/
public boolean isSkinned() {
return (GameImage.CURSOR.hasSkinImage() ||
GameImage.CURSOR_MIDDLE.hasSkinImage() ||
GameImage.CURSOR_TRAIL.hasSkinImage());
public boolean isBeatmapSkinned() {
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
}
/**

View File

@@ -0,0 +1,424 @@
/*
* 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.GameImage;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.gui.AbstractComponent;
import org.newdawn.slick.gui.GUIContext;
/**
* Simple dropdown menu.
* <p>
* Basic usage:
* <ul>
* <li>Override {@link #menuClicked(int)} to perform actions when the menu is clicked
* (e.g. play a sound effect, block input under certain conditions).
* <li>Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
* <li>Call {@link #activate()}/{@link #deactivate()} whenever the component is needed
* (e.g. in a state's {@code enter} and {@code leave} events.
* </ul>
*
* @param <E> the type of the elements in the menu
*/
public class DropdownMenu<E> extends AbstractComponent {
/** Padding ratios for drawing. */
private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;
/** Whether this component is active. */
private boolean active;
/** The menu items. */
private E[] items;
/** The menu item names. */
private String[] itemNames;
/** The index of the selected item. */
private int itemIndex = 0;
/** Whether the menu is expanded. */
private boolean expanded = false;
/** The expanding animation progress. */
private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);
/** The last update time, in milliseconds. */
private long lastUpdateTime;
/** The top-left coordinates. */
private float x, y;
/** The width and height of the dropdown menu. */
private int width, height;
/** The height of the base item. */
private int baseHeight;
/** The vertical offset between items. */
private float offsetY;
/** The colors to use. */
private Color
textColor = Color.white, backgroundColor = Color.black,
highlightColor = Colors.BLUE_DIVIDER, borderColor = Colors.BLUE_DIVIDER,
chevronDownColor = textColor, chevronRightColor = backgroundColor;
/** The fonts to use. */
private UnicodeFont fontNormal = Fonts.MEDIUM, fontSelected = Fonts.MEDIUMBOLD;
/** The chevron images. */
private Image chevronDown, chevronRight;
/**
* Creates a new dropdown menu.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y) {
this(container, items, x, y, 0);
}
/**
* Creates a new dropdown menu with the given fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, UnicodeFont normal, UnicodeFont selected) {
this(container, items, x, y, 0, normal, selected);
}
/**
* Creates a new dropdown menu with the given width.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width) {
super(container);
init(items, x, y, width);
}
/**
* Creates a new dropdown menu with the given width and fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width, UnicodeFont normal, UnicodeFont selected) {
super(container);
this.fontNormal = normal;
this.fontSelected = selected;
init(items, x, y, width);
}
/**
* Returns the maximum item width from the list.
*/
private int getMaxItemWidth() {
int maxWidth = 0;
for (int i = 0; i < itemNames.length; i++) {
int w = fontSelected.getWidth(itemNames[i]);
if (w > maxWidth)
maxWidth = w;
}
return maxWidth;
}
/**
* Initializes the component.
*/
private void init(E[] items, float x, float y, int width) {
this.items = items;
this.itemNames = new String[items.length];
for (int i = 0; i < itemNames.length; i++)
itemNames[i] = items[i].toString();
this.x = x;
this.y = y;
this.baseHeight = fontNormal.getLineHeight();
this.offsetY = baseHeight + baseHeight * PADDING_Y;
this.height = (int) (offsetY * (items.length + 1));
int chevronDownSize = baseHeight * 4 / 5;
this.chevronDown = GameImage.CHEVRON_DOWN.getImage().getScaledCopy(chevronDownSize, chevronDownSize);
int chevronRightSize = baseHeight * 2 / 3;
this.chevronRight = GameImage.CHEVRON_RIGHT.getImage().getScaledCopy(chevronRightSize, chevronRightSize);
int maxItemWidth = getMaxItemWidth();
int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
this.width = Math.max(width, minWidth);
}
@Override
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int getX() { return (int) x; }
@Override
public int getY() { return (int) y; }
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return (expanded) ? height : baseHeight; }
/** Activates the component. */
public void activate() { this.active = true; }
/** Deactivates the component. */
public void deactivate() { this.active = false; }
/**
* Returns whether the dropdown menu is currently open.
* @return true if open, false otherwise
*/
public boolean isOpen() { return expanded; }
/**
* Opens or closes the dropdown menu.
* @param flag true to open, false to close
*/
public void open(boolean flag) { this.expanded = flag; }
/**
* Returns true if the coordinates are within the menu bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean contains(float cx, float cy) {
return (cx > x && cx < x + width && (
(cy > y && cy < y + baseHeight) ||
(expanded && cy > y + offsetY && cy < y + height)));
}
/**
* Returns true if the coordinates are within the base item bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean baseContains(float cx, float cy) {
return (cx > x && cx < x + width && cy > y && cy < y + baseHeight);
}
@Override
public void render(GUIContext container, Graphics g) throws SlickException {
// update animation
long time = container.getTime();
if (lastUpdateTime > 0) {
int delta = (int) (time - lastUpdateTime);
expandProgress.update((expanded) ? delta : -delta * 2);
}
this.lastUpdateTime = time;
// get parameters
Input input = container.getInput();
int idx = getIndexAt(input.getMouseX(), input.getMouseY());
float t = expandProgress.getValue();
if (expanded)
t = AnimationEquation.OUT_CUBIC.calc(t);
// background and border
Color oldGColor = g.getColor();
float oldLineWidth = g.getLineWidth();
final int cornerRadius = 6;
g.setLineWidth(1f);
g.setColor((idx == -1) ? highlightColor : backgroundColor);
g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
g.setColor(borderColor);
g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
if (expanded || t >= 0.0001) {
float oldBackgroundAlpha = backgroundColor.a;
backgroundColor.a *= t;
g.setColor(backgroundColor);
g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
backgroundColor.a = oldBackgroundAlpha;
}
if (idx >= 0 && t >= 0.9999) {
g.setColor(highlightColor);
float yPos = y + offsetY + (offsetY * idx);
int yOff = 0, hOff = 0;
if (idx == 0 || idx == items.length - 1) {
g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
if (idx == 0)
yOff = cornerRadius;
hOff = cornerRadius;
}
g.fillRect(x, yPos + yOff, width, offsetY - hOff);
}
g.setColor(oldGColor);
g.setLineWidth(oldLineWidth);
// text
chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[itemIndex], textColor);
float oldTextAlpha = textColor.a;
textColor.a *= t;
if (expanded || t >= 0.0001) {
for (int i = 0; i < itemNames.length; i++) {
Font f = (i == itemIndex) ? fontSelected : fontNormal;
if (i == idx && t >= 0.999)
chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
}
}
textColor.a = oldTextAlpha;
}
/**
* Returns the index of the item at the given location, -1 for the base item,
* and -2 if there is no item at the location.
* @param cx the x coordinate
* @param cy the y coordinate
*/
private int getIndexAt(float cx, float cy) {
if (!contains(cx, cy))
return -2;
if (cy <= y + baseHeight)
return -1;
if (!expanded)
return -2;
return (int) ((cy - (y + offsetY)) / offsetY);
}
/**
* Resets the menu state.
*/
public void reset() {
this.expanded = false;
this.lastUpdateTime = 0;
expandProgress.setTime(0);
}
@Override
public void mousePressed(int button, int x, int y) {
if (!active)
return;
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
int idx = getIndexAt(x, y);
if (idx == -2) {
this.expanded = false;
return;
}
if (!menuClicked(idx))
return;
this.expanded = (idx == -1) ? !expanded : false;
if (idx >= 0 && itemIndex != idx) {
this.itemIndex = idx;
itemSelected(idx, items[idx]);
}
consumeEvent();
}
/**
* Notification that a new item was selected (via override).
* @param index the index of the item selected
* @param item the item selected
*/
public void itemSelected(int index, E item) {}
/**
* Notification that the menu was clicked (via override).
* @param index the index of the item clicked, or -1 for the base item
* @return true to process the click, or false to block/intercept it
*/
public boolean menuClicked(int index) { return true; }
@Override
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
@Override
public void mouseReleased(int button, int x, int y) { /* does not currently use the "focus" concept */ }
/**
* Selects the item at the given index.
* @param index the list item index
* @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size
*/
public void setSelectedIndex(int index) {
if (index < 0 || index >= items.length)
throw new IllegalArgumentException();
this.itemIndex = index;
}
/**
* Returns the index of the selected item.
*/
public int getSelectedIndex() { return itemIndex; }
/**
* Returns the selected item.
*/
public E getSelectedItem() { return items[itemIndex]; }
/**
* Returns the item at the given index.
* @param index the list item index
*/
public E getItemAt(int index) { return items[index]; }
/**
* Returns the number of items in the list.
*/
public int getItemCount() { return items.length; }
/** Sets the text color. */
public void setTextColor(Color c) { this.textColor = c; }
/** Sets the background color. */
public void setBackgroundColor(Color c) { this.backgroundColor = c; }
/** Sets the highlight color. */
public void setHighlightColor(Color c) { this.highlightColor = c; }
/** Sets the border color. */
public void setBorderColor(Color c) { this.borderColor = c; }
/** Sets the down chevron color. */
public void setChevronDownColor(Color c) { this.chevronDownColor = c; }
/** Sets the right chevron color. */
public void setChevronRightColor(Color c) { this.chevronRightColor = c; }
}

View File

@@ -0,0 +1,156 @@
/*
* 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.GameImage;
import itdelatrisu.opsu.Options;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.font.effects.Effect;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* Fonts used for drawing.
*/
public class Fonts {
public static UnicodeFont DEFAULT, BOLD, XLARGE, LARGE, MEDIUM, MEDIUMBOLD, SMALL;
/** Set of all Unicode strings already loaded per font. */
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
// This class should not be instantiated.
private Fonts() {}
/**
* Initializes all fonts.
* @throws SlickException if ASCII glyphs could not be loaded
* @throws FontFormatException if any font stream data does not contain the required font tables
* @throws IOException if a font stream cannot be completely read
*/
public static void init() throws SlickException, FontFormatException, IOException {
float fontBase = 12f * GameImage.getUIscale();
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
DEFAULT = new UnicodeFont(font);
BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
MEDIUMBOLD = new UnicodeFont(font.deriveFont(Font.BOLD, fontBase * 3 / 2));
SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(DEFAULT, colorEffect);
loadFont(BOLD, colorEffect);
loadFont(XLARGE, colorEffect);
loadFont(LARGE, colorEffect);
loadFont(MEDIUM, colorEffect);
loadFont(MEDIUMBOLD, colorEffect);
loadFont(SMALL, colorEffect);
}
/**
* Loads a Unicode font and its ASCII glyphs.
* @param font the font to load
* @param effect the font effect
* @throws SlickException if the glyphs could not be loaded
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for a font.
* @param font the font to add the glyphs to
* @param s the string containing the glyphs to load
*/
public static void loadGlyphs(UnicodeFont font, String s) {
if (s == null || s.isEmpty())
return;
// get set of added strings
HashSet<String> set = loadedGlyphs.get(font);
if (set == null) {
set = new HashSet<String>();
loadedGlyphs.put(font, set);
} else if (set.contains(s))
return; // string already in set
// load glyphs
font.addGlyphs(s);
set.add(s);
try {
font.loadGlyphs();
} catch (SlickException e) {
Log.warn(String.format("Failed to load glyphs for string '%s'.", s), e);
}
}
/**
* Wraps the given string into a list of split lines based on the width.
* @param font the font used to draw the string
* @param text the text to split
* @param width the maximum width of a line
* @return the list of split strings
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
*/
public static List<String> wrap(org.newdawn.slick.Font font, String text, int width) {
List<String> list = new ArrayList<String>();
String str = text;
String line = "";
int i = 0;
int lastSpace = -1;
while (i < str.length()) {
char c = str.charAt(i);
if (Character.isWhitespace(c))
lastSpace = i;
String append = line + c;
if (font.getWidth(append) > width) {
int split = (lastSpace != -1) ? lastSpace : i;
int splitTrimmed = split;
if (lastSpace != -1 && split < str.length() - 1)
splitTrimmed++;
list.add(str.substring(0, split));
str = str.substring(splitTrimmed);
line = "";
i = 0;
lastSpace = -1;
} else {
line = append;
i++;
}
}
if (str.length() != 0)
list.add(str);
return list;
}
}

View File

@@ -19,6 +19,8 @@
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
@@ -63,11 +65,26 @@ public class MenuButton {
/** The hover actions for this button. */
private int hoverEffect = 0;
/** The current and max scale of the button. */
private float scale = 1f, hoverScale = 1.25f;
/** The hover animation duration, in milliseconds. */
private int animationDuration = 100;
/** The current and base alpha level of the button. */
private float alpha = 1f, baseAlpha = 0.75f;
/** The hover animation equation. */
private AnimationEquation animationEqn = AnimationEquation.LINEAR;
/** Whether the animation is advancing forwards (if advancing automatically). */
private boolean autoAnimationForward = true;
/** The scale of the button. */
private AnimatedValue scale;
/** The default max scale of the button. */
private static final float DEFAULT_SCALE_MAX = 1.25f;
/** The alpha level of the button. */
private AnimatedValue alpha;
/** The default base alpha level of the button. */
private static final float DEFAULT_ALPHA_BASE = 0.75f;
/** The scaled expansion direction for the button. */
private Expand dir = Expand.CENTER;
@@ -75,8 +92,11 @@ public class MenuButton {
/** Scaled expansion directions. */
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
/** The current and max rotation angles of the button. */
private float angle = 0f, maxAngle = 30f;
/** The rotation angle of the button. */
private AnimatedValue angle;
/** The default max rotation angle of the button. */
private static final float DEFAULT_ANGLE_MAX = 30f;
/**
* Creates a new button from an Image.
@@ -126,11 +146,13 @@ public class MenuButton {
/**
* Sets a new center x coordinate.
* @param x the x coordinate
*/
public void setX(float x) { this.x = x; }
/**
* Sets a new center y coordinate.
* @param y the y coordinate
*/
public void setY(float y) { this.y = y; }
@@ -192,15 +214,15 @@ public class MenuButton {
float oldAlpha = image.getAlpha();
float oldAngle = image.getRotation();
if ((hoverEffect & EFFECT_EXPAND) > 0) {
if (scale != 1f) {
image = image.getScaledCopy(scale);
if (scale.getValue() != 1f) {
image = image.getScaledCopy(scale.getValue());
image.setAlpha(oldAlpha);
}
}
if ((hoverEffect & EFFECT_FADE) > 0)
image.setAlpha(alpha);
image.setAlpha(alpha.getValue());
if ((hoverEffect & EFFECT_ROTATE) > 0)
image.setRotation(angle);
image.setRotation(angle.getValue());
image.draw(x - xRadius, y - yRadius, filter);
if (image == this.img) {
image.setAlpha(oldAlpha);
@@ -217,9 +239,10 @@ public class MenuButton {
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
} else if ((hoverEffect & EFFECT_FADE) > 0) {
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
image.setAlpha(alpha);
imgL.setAlpha(alpha);
imgR.setAlpha(alpha);
float currentAlpha = alpha.getValue();
image.setAlpha(currentAlpha);
imgL.setAlpha(currentAlpha);
imgR.setAlpha(currentAlpha);
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
imgL.draw(x - xRadius, y - yRadius, filter);
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
@@ -267,28 +290,63 @@ public class MenuButton {
*/
public void resetHover() {
if ((hoverEffect & EFFECT_EXPAND) > 0) {
this.scale = 1f;
scale.setTime(0);
setHoverRadius();
}
if ((hoverEffect & EFFECT_FADE) > 0)
this.alpha = baseAlpha;
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
this.angle = 0f;
angle.setTime(0);
autoAnimationForward = true;
}
/**
* Removes all hover effects that have been set for the button.
*/
public void removeHoverEffects() { hoverEffect = 0; }
public void removeHoverEffects() {
this.hoverEffect = 0;
this.scale = null;
this.alpha = null;
this.angle = null;
autoAnimationForward = true;
}
/**
* Sets the hover animation duration.
* @param duration the duration, in milliseconds
*/
public void setHoverAnimationDuration(int duration) {
this.animationDuration = duration;
if (scale != null)
scale.setDuration(duration);
if (alpha != null)
alpha.setDuration(duration);
if (angle != null)
angle.setDuration(duration);
}
/**
* Sets the hover animation equation.
* @param eqn the equation to use
*/
public void setHoverAnimationEquation(AnimationEquation eqn) {
this.animationEqn = eqn;
if (scale != null)
scale.setEquation(eqn);
if (alpha != null)
alpha.setEquation(eqn);
if (angle != null)
angle.setEquation(eqn);
}
/**
* Sets the "expand" hover effect.
*/
public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; }
public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
*/
public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); }
@@ -296,45 +354,45 @@ public class MenuButton {
* Sets the "expand" hover effect.
* @param dir the expansion direction
*/
public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); }
public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
* @param dir the expansion direction
*/
public void setHoverExpand(float scale, Expand dir) {
hoverEffect |= EFFECT_EXPAND;
this.hoverScale = scale;
this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn);
this.dir = dir;
}
/**
* Sets the "fade" hover effect.
*/
public void setHoverFade() { hoverEffect |= EFFECT_FADE; }
public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); }
/**
* Sets the "fade" hover effect.
* @param baseAlpha the base alpha level to fade in from (default 0.7f)
* @param baseAlpha the base alpha level to fade in from
*/
public void setHoverFade(float baseAlpha) {
hoverEffect |= EFFECT_FADE;
this.baseAlpha = baseAlpha;
this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn);
}
/**
* Sets the "rotate" hover effect.
*/
public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; }
public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); }
/**
* Sets the "rotate" hover effect.
* @param maxAngle the maximum rotation angle, in degrees (default 30f)
* @param maxAngle the maximum rotation angle, in degrees
*/
public void setHoverRotate(float maxAngle) {
hoverEffect |= EFFECT_ROTATE;
this.maxAngle = maxAngle;
this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn);
}
/**
@@ -371,45 +429,53 @@ public class MenuButton {
if (hoverEffect == 0)
return;
int d = delta * (isHover ? 1 : -1);
// scale the button
if ((hoverEffect & EFFECT_EXPAND) > 0) {
int sign = 0;
if (isHover && scale < hoverScale)
sign = 1;
else if (!isHover && scale > 1f)
sign = -1;
if (sign != 0) {
scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale);
if (scale.update(d))
setHoverRadius();
}
}
// fade the button
if ((hoverEffect & EFFECT_FADE) > 0) {
int sign = 0;
if (isHover && alpha < 1f)
sign = 1;
else if (!isHover && alpha > baseAlpha)
sign = -1;
if (sign != 0)
alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f);
}
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.update(d);
// rotate the button
if ((hoverEffect & EFFECT_ROTATE) > 0) {
int sign = 0;
boolean right = (maxAngle > 0);
if (isHover && angle != maxAngle)
sign = (right) ? 1 : -1;
else if (!isHover && angle != 0)
sign = (right) ? -1 : 1;
if (sign != 0) {
float diff = sign * Math.abs(maxAngle) * delta / 125f;
angle = (right) ?
Utils.getBoundedValue(angle, diff, 0, maxAngle) :
Utils.getBoundedValue(angle, diff, maxAngle, 0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.update(d);
}
/**
* Automatically advances the hover animation in a loop.
* @param delta the delta interval
* @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
*/
public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
if (hoverEffect == 0)
return;
int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() :
((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() :
((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1;
if (time == -1)
return;
int d = delta * (autoAnimationForward ? 1 : -1);
if (Utils.clamp(time + d, 0, animationDuration) == time) {
if (reverseAtEnd)
autoAnimationForward = !autoAnimationForward;
else {
if ((hoverEffect & EFFECT_EXPAND) > 0)
scale.setTime(0);
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.setTime(0);
}
}
hoverUpdate(delta, autoAnimationForward);
}
/**
@@ -422,10 +488,11 @@ public class MenuButton {
image = anim.getCurrentFrame();
int xOffset = 0, yOffset = 0;
float currentScale = scale.getValue();
if (dir != Expand.CENTER) {
// offset by difference between normal/scaled image dimensions
xOffset = (int) ((scale - 1f) * image.getWidth());
yOffset = (int) ((scale - 1f) * image.getHeight());
xOffset = (int) ((currentScale - 1f) * image.getWidth());
yOffset = (int) ((currentScale - 1f) * image.getHeight());
if (dir == Expand.UP || dir == Expand.DOWN)
xOffset = 0; // no horizontal offset
if (dir == Expand.RIGHT || dir == Expand.LEFT)
@@ -435,7 +502,7 @@ public class MenuButton {
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
yOffset *= -1; // flip y for down
}
this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f;
this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f;
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.newdawn.slick.Image;
/**
* Horizontal star stream.
*/
public class StarStream {
/** The container dimensions. */
private final int containerWidth, containerHeight;
/** The star image. */
private final Image starImg;
/** The current list of stars. */
private final List<Star> stars;
/** The maximum number of stars to draw at once. */
private static final int MAX_STARS = 20;
/** Random number generator instance. */
private final Random random;
/** Contains data for a single star. */
private class Star {
/** The star animation progress. */
private final AnimatedValue animatedValue;
/** The star properties. */
private final int distance, yOffset, angle;
/**
* Creates a star with the given properties.
* @param duration the time, in milliseconds, to show the star
* @param distance the distance for the star to travel in {@code duration}
* @param yOffset the vertical offset from the center of the container
* @param angle the rotation angle
* @param eqn the animation equation to use
*/
public Star(int duration, int distance, int yOffset, int angle, AnimationEquation eqn) {
this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn);
this.distance = distance;
this.yOffset = yOffset;
this.angle = angle;
}
/**
* Draws the star.
*/
public void draw() {
float t = animatedValue.getValue();
starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f));
starImg.drawEmbedded(
containerWidth - (distance * t), ((containerHeight - starImg.getHeight()) / 2) + yOffset,
starImg.getWidth(), starImg.getHeight(), angle);
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) { return animatedValue.update(delta); }
}
/**
* Initializes the star stream.
* @param width the container width
* @param height the container height
*/
public StarStream(int width, int height) {
this.containerWidth = width;
this.containerHeight = height;
this.starImg = GameImage.STAR2.getImage().copy();
this.stars = new ArrayList<Star>();
this.random = new Random();
}
/**
* Draws the star stream.
*/
public void draw() {
if (stars.isEmpty())
return;
starImg.startUse();
for (Star star : stars)
star.draw();
starImg.endUse();
}
/**
* Updates the stars in the stream by a delta interval.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
// update current stars
Iterator<Star> iter = stars.iterator();
while (iter.hasNext()) {
Star star = iter.next();
if (!star.update(delta))
iter.remove();
}
// create new stars
for (int i = stars.size(); i < MAX_STARS; i++) {
if (Math.random() < ((i < 5) ? 0.25 : 0.66))
break;
// generate star properties
float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
int distance = (int) (containerWidth * distanceRatio);
int duration = (int) (distanceRatio * getGaussian(1300, 300));
int yOffset = (int) getGaussian(0, containerHeight / 20);
int angle = (int) getGaussian(0, 22.5);
AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
stars.add(new Star(duration, distance, angle, yOffset, eqn));
}
}
/**
* Clears the stars currently in the stream.
*/
public void clear() { stars.clear(); }
/**
* Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation.
* @param mean the mean
* @param stdDev the standard deviation
*/
private double getGaussian(double mean, double stdDev) {
return mean + random.nextGaussian() * stdDev;
}
}

View File

@@ -21,11 +21,13 @@ 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 itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
@@ -36,7 +38,6 @@ 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;
/**
@@ -62,7 +63,7 @@ public class UI {
private static int barNotifTimer = -1;
/** Duration, in milliseconds, to display bar notifications. */
private static final int BAR_NOTIFICATION_TIME = 1250;
private static final int BAR_NOTIFICATION_TIME = 1500;
/** The current tooltip. */
private static String tooltip;
@@ -70,11 +71,8 @@ public class UI {
/** 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;
/** The alpha level of the current tooltip (if any). */
private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
// game-related variables
private static GameContainer container;
@@ -87,10 +85,8 @@ public class 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 {
public static void init(GameContainer container, StateBasedGame game) {
UI.container = container;
UI.input = container.getInput();
@@ -106,6 +102,8 @@ public class UI {
Image back = GameImage.MENU_BACK.getImage();
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
}
backButton.setHoverAnimationDuration(350);
backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
}
@@ -117,12 +115,11 @@ public class UI {
cursor.update(delta);
updateVolumeDisplay(delta);
updateBarNotification(delta);
if (tooltipTimer > 0)
tooltipTimer -= delta;
tooltipAlpha.update(-delta);
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
*/
public static void draw(Graphics g) {
@@ -134,7 +131,7 @@ public class UI {
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
@@ -178,18 +175,18 @@ public class UI {
*/
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 tabTextX = x - (Fonts.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;
filter = (isHover) ? Colors.RED_HOVER : Color.red;
textColor = Color.white;
}
tabImage.drawCentered(x, y, filter);
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
}
/**
@@ -201,14 +198,14 @@ public class UI {
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),
Fonts.BOLD.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
container.getHeight() * 0.997f - Fonts.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"),
Fonts.DEFAULT.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
"FPS", Color.white
);
}
@@ -263,7 +260,7 @@ public class UI {
*/
public static void changeVolume(int units) {
final float UNIT_OFFSET = 0.05f;
Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
if (volumeDisplay == -1)
volumeDisplay = 0;
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
@@ -271,8 +268,9 @@ public class UI {
}
/**
* Draws loading progress (OSZ unpacking, beatmap parsing, sound loading)
* Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading)
* at the bottom of the screen.
* @param g the graphics context
*/
public static void drawLoadingProgress(Graphics g) {
String text, file;
@@ -298,16 +296,16 @@ public class UI {
// 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();
int lineOffsetY = Fonts.MEDIUM.getLineHeight();
if (Options.isLoadVerbose()) {
// verbose: display percentages and file names
Utils.FONT_MEDIUM.drawString(
Fonts.MEDIUM.drawString(
marginX, lineY - (lineOffsetY * 2),
String.format("%s (%d%%)", text, progress), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
} else {
// draw loading bar
Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
Fonts.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
@@ -357,12 +355,7 @@ public class UI {
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;
tooltipAlpha.update(delta * 2);
}
}
@@ -372,26 +365,26 @@ public class UI {
* @param g the graphics context
*/
public static void drawTooltip(Graphics g) {
if (tooltipTimer <= 0 || tooltip == null)
if (tooltipAlpha.getTime() == 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 lineHeight = Fonts.SMALL.getLineHeight();
int textWidth = textMarginX * 2, textHeight = lineHeight;
if (tooltipNewlines) {
String[] lines = tooltip.split("\\n");
int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
int maxWidth = Fonts.SMALL.getWidth(lines[0]);
for (int i = 1; i < lines.length; i++) {
int w = Utils.FONT_SMALL.getWidth(lines[i]);
int w = Fonts.SMALL.getWidth(lines[i]);
if (w > maxWidth)
maxWidth = w;
}
textWidth += maxWidth;
textHeight += lineHeight * (lines.length - 1);
} else
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
textWidth += Fonts.SMALL.getWidth(tooltip);
// get drawing coordinates
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
@@ -405,29 +398,29 @@ public class UI {
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;
float alpha = tooltipAlpha.getValue();
float oldAlpha = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
Colors.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);
oldAlpha = Colors.DARK_GRAY.a;
Colors.DARK_GRAY.a = alpha;
g.setColor(Colors.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;
Colors.DARK_GRAY.a = oldAlpha;
oldAlpha = Colors.WHITE_ALPHA.a;
Colors.WHITE_ALPHA.a = alpha;
Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
Colors.WHITE_ALPHA.a = oldAlpha;
}
/**
* Resets the tooltip.
*/
public static void resetTooltip() {
tooltipTimer = -1;
tooltipAlpha.setTime(0);
tooltip = null;
}
@@ -475,18 +468,18 @@ public class UI {
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);
float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
Colors.BLACK_ALPHA.a *= alpha;
Colors.WHITE_ALPHA.a = alpha;
g.setColor(Colors.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;
Fonts.LARGE.drawString(
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
midY - Fonts.LARGE.getLineHeight() / 2.2f,
barNotif, Colors.WHITE_ALPHA);
Colors.BLACK_ALPHA.a = oldAlphaB;
Colors.WHITE_ALPHA.a = oldAlphaW;
}
/**

View File

@@ -0,0 +1,134 @@
/*
* 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.animations;
import itdelatrisu.opsu.Utils;
/**
* Utility class for updating a value using an animation equation.
*/
public class AnimatedValue {
/** The animation duration, in milliseconds. */
private int duration;
/** The current time, in milliseconds. */
private int time;
/** The base value. */
private float base;
/** The maximum difference from the base value. */
private float diff;
/** The current value. */
private float value;
/** The animation equation to use. */
private AnimationEquation eqn;
/**
* Constructor.
* @param duration the total animation duration, in milliseconds
* @param min the minimum value
* @param max the maximum value
* @param eqn the animation equation to use
*/
public AnimatedValue(int duration, float min, float max, AnimationEquation eqn) {
this.time = 0;
this.duration = duration;
this.value = min;
this.base = min;
this.diff = max - min;
this.eqn = eqn;
}
/**
* Returns the current value.
*/
public float getValue() { return value; }
/**
* Returns the current animation time, in milliseconds.
*/
public int getTime() { return time; }
/**
* Sets the animation time manually.
* @param time the new time, in milliseconds
*/
public void setTime(int time) {
this.time = Utils.clamp(time, 0, duration);
updateValue();
}
/**
* Returns the total animation duration, in milliseconds.
*/
public int getDuration() { return duration; }
/**
* Sets the animation duration.
* @param duration the new duration, in milliseconds
*/
public void setDuration(int duration) {
this.duration = duration;
int newTime = Utils.clamp(time, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
}
}
/**
* Returns the animation equation being used.
*/
public AnimationEquation getEquation() { return eqn; }
/**
* Sets the animation equation to use.
* @param eqn the new equation
*/
public void setEquation(AnimationEquation eqn) {
this.eqn = eqn;
updateValue();
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call.
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) {
int newTime = Utils.clamp(time + delta, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
return true;
}
return false;
}
/**
* Recalculates the value by applying the animation equation with the current time.
*/
private void updateValue() {
float t = eqn.calc((float) time / duration);
this.value = base + (t * diff);
}
}

View File

@@ -0,0 +1,308 @@
/*
* 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.animations;
/*
* These equations are copyright (c) 2001 Robert Penner, all rights reserved,
* and are open source under the BSD License.
* http://www.opensource.org/licenses/bsd-license.php
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Easing functions for animations.
*
* @author Robert Penner (<a href="http://robertpenner.com/easing/">http://robertpenner.com/easing/</a>)
* @author CharlotteGore (<a href="https://github.com/CharlotteGore/functional-easing">https://github.com/CharlotteGore/functional-easing</a>)
*/
public enum AnimationEquation {
/* Linear */
LINEAR {
@Override
public float calc(float t) { return t; }
},
/* Quadratic */
IN_QUAD {
@Override
public float calc(float t) { return t * t; }
},
OUT_QUAD {
@Override
public float calc(float t) { return -1 * t * (t - 2); }
},
IN_OUT_QUAD {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t;
t = t - 1;
return -0.5f * (t * (t - 2) - 1);
}
},
/* Cubic */
IN_CUBIC {
@Override
public float calc(float t) { return t * t * t; }
},
OUT_CUBIC {
@Override
public float calc(float t) {
t = t - 1;
return t * t * t + 1;
}
},
IN_OUT_CUBIC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t;
t = t - 2;
return 0.5f * (t * t * t + 2);
}
},
/* Quartic */
IN_QUART {
@Override
public float calc(float t) { return t * t * t * t; }
},
OUT_QUART {
@Override
public float calc(float t) {
t = t - 1;
return -1 * (t * t * t * t - 1);
}
},
IN_OUT_QUART {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t;
t = t - 2;
return -0.5f * (t * t * t * t - 2);
}
},
/* Quintic */
IN_QUINT {
@Override
public float calc(float t) { return t * t * t * t * t; }
},
OUT_QUINT {
@Override
public float calc(float t) {
t = t - 1;
return (t * t * t * t * t + 1);
}
},
IN_OUT_QUINT {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t * t;
t = t - 2;
return 0.5f * (t * t * t * t * t + 2);
}
},
/* Sine */
IN_SINE {
@Override
public float calc(float t) { return -1 * (float) Math.cos(t * (Math.PI / 2)) + 1; }
},
OUT_SINE {
@Override
public float calc(float t) { return (float) Math.sin(t * (Math.PI / 2)); }
},
IN_OUT_SINE {
@Override
public float calc(float t) { return (float) (Math.cos(Math.PI * t) - 1) / -2; }
},
/* Exponential */
IN_EXPO {
@Override
public float calc(float t) { return (t == 0) ? 0 : (float) Math.pow(2, 10 * (t - 1)); }
},
OUT_EXPO {
@Override
public float calc(float t) { return (t == 1) ? 1 : (float) -Math.pow(2, -10 * t) + 1; }
},
IN_OUT_EXPO {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
t = t * 2;
if (t < 1)
return 0.5f * (float) Math.pow(2, 10 * (t - 1));
t = t - 1;
return 0.5f * ((float) -Math.pow(2, -10 * t) + 2);
}
},
/* Circular */
IN_CIRC {
@Override
public float calc(float t) { return -1 * ((float) Math.sqrt(1 - t * t) - 1); }
},
OUT_CIRC {
@Override
public float calc(float t) {
t = t - 1;
return (float) Math.sqrt(1 - t * t);
}
},
IN_OUT_CIRC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return -0.5f * ((float) Math.sqrt(1 - t * t) - 1);
t = t - 2;
return 0.5f * ((float) Math.sqrt(1 - t * t) + 1);
}
},
/* Back */
IN_BACK {
@Override
public float calc(float t) { return t * t * ((OVERSHOOT + 1) * t - OVERSHOOT); }
},
OUT_BACK {
@Override
public float calc(float t) {
t = t - 1;
return t * t * ((OVERSHOOT + 1) * t + OVERSHOOT) + 1;
}
},
IN_OUT_BACK {
@Override
public float calc(float t) {
float overshoot = OVERSHOOT * 1.525f;
t = t * 2;
if (t < 1)
return 0.5f * (t * t * ((overshoot + 1) * t - overshoot));
t = t - 2;
return 0.5f * (t * t * ((overshoot + 1) * t + overshoot) + 2);
}
},
/* Bounce */
IN_BOUNCE {
@Override
public float calc(float t) { return 1 - OUT_BOUNCE.calc(1 - t); }
},
OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.36363636f)
return 7.5625f * t * t;
else if (t < 0.72727273f) {
t = t - 0.54545454f;
return 7.5625f * t * t + 0.75f;
} else if (t < 0.90909091f) {
t = t - 0.81818182f;
return 7.5625f * t * t + 0.9375f;
} else {
t = t - 0.95454546f;
return 7.5625f * t * t + 0.984375f;
}
}
},
IN_OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.5f)
return IN_BOUNCE.calc(t * 2) * 0.5f;
return OUT_BOUNCE.calc(t * 2 - 1) * 0.5f + 0.5f;
}
},
/* Elastic */
IN_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
t = t - 1;
return -((float) Math.pow(2, 10 * t) * (float) Math.sin(((t - period / 4) * (Math.PI * 2)) / period));
}
},
OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) + 1;
}
},
IN_OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.44999996f;
t = t * 2 - 1;
if (t < 0)
return -0.5f * ((float) Math.pow(2, 10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period));
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) * 0.5f + 1;
}
};
/** Overshoot constant for "back" easings. */
private static final float OVERSHOOT = 1.70158f;
/**
* Calculates a new {@code t} value using the animation equation.
* @param t the raw {@code t} value [0,1]
* @return the new {@code t} value [0,1]
*/
public abstract float calc(float t);
}