From 6e7de654b0995f861a9714d9065d172c450526df Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 18:40:10 -0500 Subject: [PATCH 01/40] Hit animation bug fixes. - In "Hidden" mod, don't draw expanding animation for circles. (related to #121) - With "Show Perfect Hits" disabled, still show the hit animations instead of having the objects just disappear. (also mentioned in #121) - Removed the two easing functions in Utils (and replaced references with calls to AnimationEquations). Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 84 ++++++++++--------- src/itdelatrisu/opsu/Utils.java | 26 ------ .../opsu/ui/animations/AnimationEquation.java | 1 - 3 files changed, 43 insertions(+), 68 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 56c8566f..c24e9256 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -28,6 +28,7 @@ import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import java.util.Date; @@ -255,6 +256,9 @@ private class HitObjectResult { /** Whether or not to expand when animating. */ public boolean expand; + /** Whether or not to hide the hit result. */ + public boolean hideResult; + /** * Constructor. * @param time the result's starting track position @@ -264,9 +268,10 @@ private class HitObjectResult { * @param color the color of the hit object * @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 +280,7 @@ public HitObjectResult(int time, int result, float x, float y, Color color, this.hitResultType = hitResultType; this.curve = curve; this.expand = expand; + this.hideResult = hideResult; } } @@ -872,7 +878,7 @@ public void drawHitResults(int trackPosition) { } // 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,14 +891,10 @@ else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && hitResult.hitResultType == HitObjectType.CIRCLE || hitResult.hitResultType == HitObjectType.SLIDER_FIRST || hitResult.hitResultType == HitObjectType.SLIDER_LAST)) { - float scale = (!hitResult.expand) ? 1f : Utils.easeOut( - Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME), - 1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME - ); - float alpha = Utils.easeOut( - Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME), - 1f, -1f, HITCIRCLE_FADE_TIME - ); + 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) { @@ -906,26 +908,28 @@ else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS && } // 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); @@ -1207,7 +1211,7 @@ public void sliderTickResult(int time, int result, float x, float y, HitObject h 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++; } @@ -1363,20 +1367,18 @@ public void hitResult(int time, int result, float x, float y, Color color, 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)); - - // 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)); - } + if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) + return; // "relax" and "autopilot" mods: hide misses + + 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); + 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, hideResult)); } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 1737a10e..e0f41197 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -654,32 +654,6 @@ else if (seconds < 3600) 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 diff --git a/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java index a30c5b30..3953b436 100644 --- a/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java +++ b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java @@ -18,7 +18,6 @@ package itdelatrisu.opsu.ui.animations; - /* * These equations are copyright (c) 2001 Robert Penner, all rights reserved, * and are open source under the BSD License. From 5f5c0cdbc4f397da622905a7e3fee6093e8555b2 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 18:54:18 -0500 Subject: [PATCH 02/40] Fix program crashing if unable to connect to BeatmapDB. (fixes #111) In this case, load all beatmaps from disk instead of trying to read from the cache. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/beatmap/BeatmapParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index ef403b03..be729980 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -134,7 +134,7 @@ public boolean accept(File dir, String name) { // check if beatmap is cached String path = String.format("%s/%s", dir.getName(), file.getName()); - if (map.containsKey(path)) { + if (map != null && map.containsKey(path)) { // check last modified times long lastModified = map.get(path); if (lastModified == file.lastModified()) { From 91f07855a78f9f4446688accb7f5905a16889075 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 20:02:23 -0500 Subject: [PATCH 03/40] Moved all Utils.COLOR_* into new class opsu.ui.Colors.*. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 37 +++++++------- src/itdelatrisu/opsu/Utils.java | 21 -------- .../opsu/beatmap/BeatmapSetNode.java | 5 +- .../opsu/downloads/DownloadNode.java | 5 +- src/itdelatrisu/opsu/objects/Circle.java | 11 +++-- src/itdelatrisu/opsu/objects/Slider.java | 23 ++++----- src/itdelatrisu/opsu/objects/Spinner.java | 9 ++-- .../opsu/objects/curves/Curve.java | 4 +- .../opsu/render/CurveRenderState.java | 6 +-- .../opsu/states/DownloadsMenu.java | 3 +- src/itdelatrisu/opsu/states/Game.java | 11 +++-- src/itdelatrisu/opsu/states/MainMenu.java | 9 ++-- src/itdelatrisu/opsu/states/OptionsMenu.java | 5 +- src/itdelatrisu/opsu/states/SongMenu.java | 25 +++++----- src/itdelatrisu/opsu/ui/Colors.java | 49 +++++++++++++++++++ src/itdelatrisu/opsu/ui/UI.java | 40 +++++++-------- 16 files changed, 151 insertions(+), 112 deletions(-) create mode 100644 src/itdelatrisu/opsu/ui/Colors.java diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index c24e9256..af22019b 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -28,6 +28,7 @@ import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; @@ -620,7 +621,7 @@ public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObjec ); } 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 ); @@ -657,27 +658,27 @@ public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObjec 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); @@ -836,7 +837,7 @@ scoreTextScale, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false // 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; @@ -898,12 +899,12 @@ else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.re // 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; } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index e0f41197..09deb763 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -60,7 +60,6 @@ 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; @@ -77,26 +76,6 @@ * 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, diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java index 6324d1c4..1272217f 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java @@ -22,6 +22,7 @@ import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.ui.Colors; import org.newdawn.slick.Color; import org.newdawn.slick.Image; @@ -78,10 +79,10 @@ public void draw(float x, float y, Grade grade, boolean focus) { 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); diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 302b740e..40f3838d 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -26,6 +26,7 @@ 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.UI; import java.io.File; @@ -329,7 +330,7 @@ public void drawResult(Graphics g, int index, boolean hover, boolean focus, bool // 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); } @@ -337,7 +338,7 @@ public void drawResult(Graphics g, int index, boolean hover, boolean focus, bool 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); } } diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 9c9320fd..7818ba58 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.states.Game; +import itdelatrisu.opsu.ui.Colors; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -104,21 +105,21 @@ public void draw(Graphics g, int trackPosition) { alpha = Math.min(alpha, fadeOutAlpha); } - 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; 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; } /** diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index f022fda6..2fe24b73 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -31,6 +31,7 @@ import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.LinearBezier; import itdelatrisu.opsu.states.Game; +import itdelatrisu.opsu.ui.Colors; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -184,8 +185,8 @@ public void draw(Graphics g, int trackPosition) { 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); @@ -195,19 +196,19 @@ public void draw(Graphics g, int trackPosition) { // end circle hitCircle.drawCentered(endPos[0], endPos[1], color); - hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE); + hitCircleOverlay.drawCentered(endPos[0], endPos[1], 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); + tick.drawCentered(c[0], c[1], Colors.WHITE_FADE); } } if (GameMod.HIDDEN.isActive()) { @@ -221,7 +222,7 @@ public void draw(Graphics g, int trackPosition) { 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++) { @@ -273,16 +274,16 @@ public void draw(Graphics g, int trackPosition) { // "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; } /** diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 1677381e..53d67639 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -28,6 +28,7 @@ import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.states.Game; +import itdelatrisu.opsu.ui.Colors; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -183,12 +184,12 @@ public void draw(Graphics g, int trackPosition) { // 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 diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index ad79353a..fad50877 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -20,10 +20,10 @@ 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; @@ -116,7 +116,7 @@ public void draw(Color color) { 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); } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 99a6f384..3696a056 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -18,9 +18,9 @@ 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; @@ -127,14 +127,14 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { 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, oldTex); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb); GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); - Utils.COLOR_WHITE_FADE.a = alpha; + Colors.WHITE_FADE.a = alpha; } // draw a fullscreen quad with the texture that contains the curve diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index a92e548b..cc7cf779 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -37,6 +37,7 @@ import itdelatrisu.opsu.downloads.servers.HexideServer; import itdelatrisu.opsu.downloads.servers.MnetworkServer; import itdelatrisu.opsu.downloads.servers.YaSOnlineServer; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -424,7 +425,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics 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); diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 73a88901..d89102fa 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -46,6 +46,7 @@ import itdelatrisu.opsu.replay.PlaybackSpeed; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimationEquation; @@ -475,15 +476,15 @@ else if (breakIndex > 1) { 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; + Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f; Utils.FONT_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()) @@ -560,7 +561,7 @@ else if (breakIndex > 1) { // returning from pause screen if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) { // 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 diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index e8f186a6..34f51b79 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -31,6 +31,7 @@ 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.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; import itdelatrisu.opsu.ui.UI; @@ -246,12 +247,12 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) } // 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(); diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 2e8df7d1..e45d076b 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -248,7 +249,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // 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) ? @@ -420,7 +421,7 @@ private void drawOption(GameOption option, int pos, boolean focus) { 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); + g.setColor(Colors.WHITE_ALPHA); g.drawLine(0, y + textHeight, width, y + textHeight); } diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 681035d6..7d63a70a 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -39,6 +39,7 @@ import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimatedValue; @@ -317,10 +318,10 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) g.clearClip(); // 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); @@ -353,14 +354,14 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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; + (speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.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; + (multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT; Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4); } @@ -413,18 +414,18 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) float searchExtraHeight = Utils.FONT_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; + Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH); if (searchEmpty) Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white); else { @@ -449,14 +450,14 @@ else if (startNode.index == focusNode.index) startIndex += startNode.beatmapIndex; UI.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS, width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset, - Utils.COLOR_BLACK_ALPHA, Color.white, true); + Colors.BLACK_ALPHA, Color.white, true); } } // 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); diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java new file mode 100644 index 00000000..8e87f1d3 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/Colors.java @@ -0,0 +1,49 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui; + +import org.newdawn.slick.Color; + +/** + * Colors used for drawing. + */ +public class Colors { + /** Game 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); + + // This class should not be instantiated. + private Colors() {} +} diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 34bd316f..c4785c65 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -185,7 +185,7 @@ public static void drawTab(float x, float y, String text, boolean selected, bool 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); @@ -404,21 +404,21 @@ else if (y < margin) // draw tooltip text inside a filled rectangle float alpha = tooltipAlpha.getValue(); - 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 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; + Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA); + Colors.WHITE_ALPHA.a = oldAlpha; } /** @@ -474,17 +474,17 @@ public static void drawBarNotification(Graphics g) { 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 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; + barNotif, Colors.WHITE_ALPHA); + Colors.BLACK_ALPHA.a = oldAlphaB; + Colors.WHITE_ALPHA.a = oldAlphaW; } /** From 7b5b96752c65ea523c002cd0438537318db1f6f0 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 20:14:24 -0500 Subject: [PATCH 04/40] Moved some duplicate colors into the Colors class. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ScoreData.java | 8 ++------ src/itdelatrisu/opsu/downloads/DownloadNode.java | 14 ++++---------- src/itdelatrisu/opsu/states/MainMenu.java | 7 +------ src/itdelatrisu/opsu/ui/Colors.java | 12 +++++++----- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 881a6fe2..004e87f6 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -20,6 +20,7 @@ import itdelatrisu.opsu.GameData.Grade; import itdelatrisu.opsu.states.SongMenu; +import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.UI; import java.sql.ResultSet; @@ -84,11 +85,6 @@ public class ScoreData implements Comparable { /** Drawing values. */ private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset; - /** 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 @@ -238,7 +234,7 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f; // rectangle outline - g.setColor((focus) ? BG_FOCUS : BG_NORMAL); + g.setColor((focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL); g.fillRect(baseX, y, buttonWidth, buttonHeight); // rank diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 40f3838d..260ed209 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -69,12 +69,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 @@ -207,7 +201,7 @@ public static boolean downloadAreaContains(float cx, float cy) { */ public static void drawResultScrollbar(Graphics g, int index, int total) { UI.drawScrollbar(g, index, total, maxResultsShown, buttonBaseX, buttonBaseY, - buttonWidth * 1.01f, buttonHeight, buttonOffset, BG_NORMAL, Color.white, true); + buttonWidth * 1.01f, buttonHeight, buttonOffset, Colors.BLACK_BG_NORMAL, Color.white, true); } /** @@ -218,7 +212,7 @@ public static void drawResultScrollbar(Graphics g, int index, int total) { */ public static void drawDownloadScrollbar(Graphics g, int index, int total) { UI.drawScrollbar(g, index, total, maxDownloadsShown, infoBaseX, infoBaseY, - infoWidth, infoHeight, infoHeight, BG_NORMAL, Color.white, true); + infoWidth, infoHeight, infoHeight, Colors.BLACK_BG_NORMAL, Color.white, true); } /** @@ -325,7 +319,7 @@ public void drawResult(Graphics g, int index, boolean hover, boolean focus, bool 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 @@ -386,7 +380,7 @@ public void drawDownload(Graphics g, int index, int id, boolean hover) { 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 diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 34f51b79..2283ee7e 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -117,11 +117,6 @@ private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING } /** 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; @@ -274,7 +269,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // 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) { diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java index 8e87f1d3..2f79efb9 100644 --- a/src/itdelatrisu/opsu/ui/Colors.java +++ b/src/itdelatrisu/opsu/ui/Colors.java @@ -24,7 +24,6 @@ * Colors used for drawing. */ public class Colors { - /** Game colors. */ public static final Color BLACK_ALPHA = new Color(0, 0, 0, 0.5f), WHITE_ALPHA = new Color(255, 255, 255, 0.5f), @@ -36,13 +35,16 @@ public class Colors { 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), + 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); + 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() {} From 0a7aef734771ae2703b2d9a413775aec53062bf6 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 20:40:07 -0500 Subject: [PATCH 05/40] Moved all Utils.FONT_* into new class opsu.ui.Fonts.*. - Moved Utils.loadGlyphs() into this class, and rewrote it to take a single generic string (instead of beatmap title/artist strings, specifically). - Also moved font initialization into this class. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 7 +- src/itdelatrisu/opsu/GameImage.java | 8 +- src/itdelatrisu/opsu/GameMod.java | 7 +- src/itdelatrisu/opsu/Options.java | 7 +- src/itdelatrisu/opsu/ScoreData.java | 37 +++--- src/itdelatrisu/opsu/Utils.java | 81 +------------ .../opsu/beatmap/BeatmapSetNode.java | 12 +- .../opsu/downloads/DownloadNode.java | 33 ++--- src/itdelatrisu/opsu/states/ButtonMenu.java | 23 ++-- .../opsu/states/DownloadsMenu.java | 35 +++--- src/itdelatrisu/opsu/states/Game.java | 3 +- src/itdelatrisu/opsu/states/MainMenu.java | 11 +- src/itdelatrisu/opsu/states/OptionsMenu.java | 25 ++-- src/itdelatrisu/opsu/states/SongMenu.java | 44 +++---- src/itdelatrisu/opsu/ui/Fonts.java | 113 ++++++++++++++++++ src/itdelatrisu/opsu/ui/UI.java | 42 +++---- 16 files changed, 271 insertions(+), 217 deletions(-) create mode 100644 src/itdelatrisu/opsu/ui/Fonts.java diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index af22019b..d0f95b69 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -29,6 +29,7 @@ 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; @@ -841,12 +842,12 @@ scoreTextScale, getScoreSymbolImage('0').getWidth() * scoreTextScale - 2, false 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 diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 6e6aa8fb..112524f0 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -18,6 +18,8 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.ui.Fonts; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -251,14 +253,14 @@ protected Image process_sub(Image img, int w, int h) { 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); } }, @@ -312,7 +314,7 @@ protected Image process_sub(Image img, int w, int h) { 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); } }, diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index 9a2dc2b6..cf28e809 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.animations.AnimationEquation; @@ -97,10 +98,10 @@ public enum Category { * @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; } /** @@ -193,7 +194,7 @@ public static void init(int width, int height) { 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(); diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 39d2a775..c864ff97 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -22,6 +22,7 @@ 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; @@ -287,9 +288,9 @@ public void click(GameContainer container) { 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); } diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 004e87f6..a9a1b673 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -21,6 +21,7 @@ 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 java.sql.ResultSet; @@ -95,7 +96,7 @@ public static void init(int containerWidth, float topY) { 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; } @@ -231,7 +232,7 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) float edgeX = baseX + buttonWidth * 0.98f; float y = baseY + index * (buttonOffset); float midY = y + buttonHeight / 2f; - float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f; + float marginY = Fonts.DEFAULT.getLineHeight() * 0.01f; // rectangle outline g.setColor((focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL); @@ -239,9 +240,9 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) // rank if (focus) { - Utils.FONT_LARGE.drawString( + Fonts.LARGE.drawString( baseX + buttonWidth * 0.04f, - y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f, + y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2f, Integer.toString(rank + 1), Color.white ); } @@ -250,8 +251,8 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) img.drawCentered(baseX + buttonWidth * 0.15f, midY); // score - float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f; - Utils.FONT_MEDIUM.drawString( + 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), Color.white @@ -259,8 +260,8 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) // 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(), + 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), Color.white ); @@ -276,26 +277,26 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) if (sb.length() > 0) { sb.setLength(sb.length() - 1); String modString = sb.toString(); - Utils.FONT_DEFAULT.drawString( - edgeX - Utils.FONT_DEFAULT.getWidth(modString), + Fonts.DEFAULT.drawString( + edgeX - Fonts.DEFAULT.getWidth(modString), y + marginY, modString, Color.white ); } // accuracy String accuracy = String.format("%.2f%%", getScorePercent()); - Utils.FONT_DEFAULT.drawString( - edgeX - Utils.FONT_DEFAULT.getWidth(accuracy), - y + marginY + Utils.FONT_DEFAULT.getLineHeight(), + Fonts.DEFAULT.drawString( + edgeX - Fonts.DEFAULT.getWidth(accuracy), + y + marginY + Fonts.DEFAULT.getLineHeight(), accuracy, Color.white ); // 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, + Fonts.DEFAULT.drawString( + edgeX - Fonts.DEFAULT.getWidth(diff), + y + marginY + Fonts.DEFAULT.getLineHeight() * 2, diff, Color.white ); @@ -303,9 +304,9 @@ public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) if (getTimeSince() != null) { Image clock = GameImage.HISTORY.getImage(); clock.drawCentered(baseX + buttonWidth * 1.02f + clock.getWidth() / 2f, midY); - Utils.FONT_DEFAULT.drawString( + Fonts.DEFAULT.drawString( baseX + buttonWidth * 1.03f + clock.getWidth(), - midY - Utils.FONT_DEFAULT.getLineHeight() / 2f, + midY - Fonts.DEFAULT.getLineHeight() / 2f, getTimeSince(), Color.white ); } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 09deb763..6126dcc0 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -24,9 +24,9 @@ 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; @@ -46,8 +46,6 @@ 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; @@ -63,12 +61,8 @@ 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; @@ -76,14 +70,6 @@ * Contains miscellaneous utilities. */ public class Utils { - /** 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> loadedGlyphs = new HashMap>(); - /** * List of illegal filename characters. * @see #cleanFileName(String, char) @@ -128,23 +114,8 @@ public static void init(GameContainer container, StateBasedGame game) 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); } @@ -326,54 +297,6 @@ public void run() { }.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 set = loadedGlyphs.get(font); - if (set == null) { - set = new HashSet(); - 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 diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java index 1272217f..3acbc850 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java @@ -21,8 +21,8 @@ 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; @@ -99,14 +99,14 @@ public void draw(float x, float y, Grade grade, boolean focus) { // 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() - 2, String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor); if (expanded || beatmapSet.size() == 1) - Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 4, + Fonts.BOLD.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 4, beatmap.version, textColor); } diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 260ed209..cf9775c7 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -27,6 +27,7 @@ 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; @@ -81,16 +82,16 @@ public static void init(int width, int height) { 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); @@ -257,7 +258,7 @@ public void error() { }); this.download = download; if (Options.useUnicodeMetadata()) // load glyphs - Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null); + Fonts.loadGlyphs(Fonts.LARGE, getTitle()); } /** @@ -344,19 +345,21 @@ public void drawResult(Graphics g, int index, boolean hover, boolean focus, bool // 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()); - g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight()); - Utils.FONT_BOLD.drawString( + if (Options.useUnicodeMetadata()) { // load glyphs + Fonts.loadGlyphs(Fonts.BOLD, getTitle()); + Fonts.loadGlyphs(Fonts.BOLD, getArtist()); + } + 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); } @@ -398,8 +401,8 @@ else if (status == Download.Status.WAITING) 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) { diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index de363c57..1892e0e7 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -29,6 +29,7 @@ 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; @@ -189,15 +190,15 @@ public void draw(GameContainer container, StateBasedGame game, Graphics g) { 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()); } @@ -293,7 +294,7 @@ public void init(GameContainer container, StateBasedGame game, Image button, Ima 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; } @@ -306,7 +307,7 @@ public void init(GameContainer container, StateBasedGame game, Image button, Ima */ 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; } @@ -320,9 +321,9 @@ public void draw(GameContainer container, StateBasedGame game, Graphics g) { // 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 @@ -418,8 +419,8 @@ public void enter(GameContainer container, StateBasedGame game) { 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 list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth); + if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) { + List list = Utils.wrap(title[i], Fonts.LARGE, maxLineWidth); actualTitle.addAll(list); } else actualTitle.add(title[i]); diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index cc7cf779..50f37fa9 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -38,6 +38,7 @@ import itdelatrisu.opsu.downloads.servers.MnetworkServer; import itdelatrisu.opsu.downloads.servers.YaSOnlineServer; import itdelatrisu.opsu.ui.Colors; +import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -261,17 +262,17 @@ public void init(GameContainer container, StateBasedGame game) 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); @@ -296,7 +297,7 @@ public void init(GameContainer container, StateBasedGame game) 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(); @@ -322,9 +323,9 @@ public void init(GameContainer container, StateBasedGame game) 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(); @@ -343,13 +344,13 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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 ); @@ -377,9 +378,9 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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) @@ -391,10 +392,10 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // 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(); @@ -417,9 +418,9 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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.setText(SERVERS[serverIndex].getName(), Fonts.MEDIUM, Color.white); serverButton.draw(Color.blue); // importing beatmaps diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index d89102fa..823f359d 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -47,6 +47,7 @@ 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; @@ -479,7 +480,7 @@ else if (breakIndex > 1) { float oldAlpha = Colors.WHITE_FADE.a; if (timeDiff < -500) Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f; - Utils.FONT_MEDIUM.drawString( + Fonts.MEDIUM.drawString( 2 + (width / 100), retryHeight, String.format("%d retries and counting...", retries), Colors.WHITE_FADE diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 2283ee7e..83d4951c 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -32,6 +32,7 @@ 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; @@ -292,15 +293,17 @@ else if (status == Updater.Status.UPDATE_DOWNLOADED) // 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)); } diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index e45d076b..60a48ade 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -27,6 +27,7 @@ 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; @@ -185,8 +186,8 @@ public void init(GameContainer container, StateBasedGame game) // 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()) @@ -210,8 +211,8 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // 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); // game options @@ -255,9 +256,9 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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 ); } @@ -414,13 +415,13 @@ public void enter(GameContainer container, StateBasedGame game) */ 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); + 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); } @@ -434,7 +435,7 @@ private GameOption getOptionAt(int y) { 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; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 7d63a70a..44cd4834 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -40,6 +40,7 @@ import itdelatrisu.opsu.db.ScoreDB; 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.UI; import itdelatrisu.opsu.ui.animations.AnimatedValue; @@ -226,8 +227,8 @@ public void init(GameContainer container, StateBasedGame game) // 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 @@ -248,11 +249,11 @@ public void init(GameContainer container, StateBasedGame game) 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); @@ -343,26 +344,27 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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; 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; + Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white); + headerTextY += Fonts.LARGE.getLineHeight() - 6; + Fonts.DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white); + headerTextY += Fonts.DEFAULT.getLineHeight() - 2; float speedModifier = GameMod.getSpeedMultiplier(); Color color2 = (speedModifier == 1f) ? Color.white : (speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.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; + Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2); + headerTextY += Fonts.BOLD.getLineHeight() - 4; + Fonts.DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white); + headerTextY += Fonts.DEFAULT.getLineHeight() - 4; float multiplier = GameMod.getDifficultyMultiplier(); Color color4 = (multiplier == 1f) ? Color.white : (multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT; - Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4); + Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4); } // score buttons @@ -410,8 +412,8 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) 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 = Colors.BLACK_ALPHA.a; @@ -425,16 +427,16 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) g.setColor(Colors.BLACK_ALPHA); g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight); Colors.BLACK_ALPHA.a = oldAlpha; - Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH); + 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); } diff --git a/src/itdelatrisu/opsu/ui/Fonts.java b/src/itdelatrisu/opsu/ui/Fonts.java new file mode 100644 index 00000000..5513ae90 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/Fonts.java @@ -0,0 +1,113 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.ui; + +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.Options; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; + +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, SMALL; + + /** Set of all Unicode strings already loaded per font. */ + private static HashMap> loadedGlyphs = new HashMap>(); + + // 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)); + 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(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 set = loadedGlyphs.get(font); + if (set == null) { + set = new HashSet(); + 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); + } + } +} diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index c4785c65..64471614 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -178,7 +178,7 @@ public static void enter() { */ 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) { @@ -189,7 +189,7 @@ public static void drawTab(float x, float y, String text, boolean selected, bool 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 +201,14 @@ public static void drawFPS() { 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 ); } @@ -298,16 +298,16 @@ public static void drawLoadingProgress(Graphics g) { // 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 @@ -376,20 +376,20 @@ public static void drawTooltip(Graphics g) { 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; @@ -417,7 +417,7 @@ else if (y < margin) Colors.DARK_GRAY.a = oldAlpha; oldAlpha = Colors.WHITE_ALPHA.a; Colors.WHITE_ALPHA.a = alpha; - Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA); + Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA); Colors.WHITE_ALPHA.a = oldAlpha; } @@ -473,15 +473,15 @@ public static void drawBarNotification(Graphics g) { 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 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, + 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; From 441bb95a099cebd5dcbf451359d70072d2dd7240 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 21:11:55 -0500 Subject: [PATCH 06/40] Added lots of 'final' modifiers to class fields where applicable. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 30 +++++++++---------- src/itdelatrisu/opsu/GameImage.java | 8 ++--- src/itdelatrisu/opsu/GameMod.java | 26 ++++++++-------- src/itdelatrisu/opsu/Options.java | 8 ++--- src/itdelatrisu/opsu/audio/HitSound.java | 6 ++-- src/itdelatrisu/opsu/audio/MultiClip.java | 2 +- src/itdelatrisu/opsu/audio/SoundEffect.java | 2 +- .../opsu/beatmap/BeatmapSortOrder.java | 6 ++-- src/itdelatrisu/opsu/downloads/Download.java | 2 +- src/itdelatrisu/opsu/downloads/Updater.java | 2 +- src/itdelatrisu/opsu/replay/LifeFrame.java | 4 +-- .../opsu/replay/PlaybackSpeed.java | 8 ++--- src/itdelatrisu/opsu/replay/ReplayFrame.java | 6 ++-- src/itdelatrisu/opsu/states/ButtonMenu.java | 8 ++--- .../opsu/states/DownloadsMenu.java | 6 ++-- src/itdelatrisu/opsu/states/Game.java | 2 +- .../opsu/states/GamePauseMenu.java | 2 +- src/itdelatrisu/opsu/states/GameRanking.java | 2 +- src/itdelatrisu/opsu/states/MainMenu.java | 2 +- src/itdelatrisu/opsu/states/OptionsMenu.java | 6 ++-- src/itdelatrisu/opsu/states/SongMenu.java | 2 +- src/itdelatrisu/opsu/states/Splash.java | 2 +- 22 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index d0f95b69..a9473715 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -90,7 +90,7 @@ public enum Grade { 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; @@ -202,14 +202,14 @@ public Image getMenuImage() { */ 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. @@ -235,31 +235,31 @@ public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINN /** 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; - - /** Alpha level (for fading out). */ - public float alpha = 1f; + public final HitObjectType hitResultType; /** Slider curve. */ - public Curve curve; + public final Curve curve; /** Whether or not to expand when animating. */ - public boolean expand; + public final boolean expand; /** Whether or not to hide the hit result. */ - public boolean hideResult; + public final boolean hideResult; + + /** Alpha level (for fading out). */ + public float alpha = 1f; /** * Constructor. diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 112524f0..8d0dda19 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -359,22 +359,22 @@ protected Image process_sub(Image img, int w, int h) { 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 skinnable; /** Whether or not to preload the image when the program starts. */ - private boolean preload; + private final boolean preload; /** The default image. */ private Image defaultImage; diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index cf28e809..4ef301ae 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -69,13 +69,13 @@ public enum Category { 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; @@ -126,37 +126,37 @@ public void init(int width, int height) { } /** 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; + private final float multiplier; /** Whether or not the mod is implemented. */ - private boolean implemented; + private final boolean implemented; /** 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; diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index c864ff97..9d824f26 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -459,13 +459,13 @@ public String getValueString() { DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", 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; @@ -487,7 +487,7 @@ private enum OptionType { BOOLEAN, NUMERIC, OTHER }; * @param displayName the option name, as displayed in the configuration file */ GameOption(String displayName) { - this.displayName = displayName; + this(null, displayName, null); } /** diff --git a/src/itdelatrisu/opsu/audio/HitSound.java b/src/itdelatrisu/opsu/audio/HitSound.java index 590f3ddb..6d0058dd 100644 --- a/src/itdelatrisu/opsu/audio/HitSound.java +++ b/src/itdelatrisu/opsu/audio/HitSound.java @@ -40,10 +40,10 @@ public enum SampleSet { // 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 SampleSet { 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 clips; diff --git a/src/itdelatrisu/opsu/audio/MultiClip.java b/src/itdelatrisu/opsu/audio/MultiClip.java index 0d4da1c6..06e7ae22 100644 --- a/src/itdelatrisu/opsu/audio/MultiClip.java +++ b/src/itdelatrisu/opsu/audio/MultiClip.java @@ -49,7 +49,7 @@ public class MultiClip { private byte[] audioData; /** The name given to this clip. */ - private String name; + private final String name; /** * Constructor. diff --git a/src/itdelatrisu/opsu/audio/SoundEffect.java b/src/itdelatrisu/opsu/audio/SoundEffect.java index 03cabe3d..85aa64a9 100644 --- a/src/itdelatrisu/opsu/audio/SoundEffect.java +++ b/src/itdelatrisu/opsu/audio/SoundEffect.java @@ -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; diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java index a9c3c545..ad1d53c5 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSortOrder.java @@ -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 comparator; + private final Comparator comparator; /** The tab associated with the sort (displayed in Song Menu screen). */ private MenuButton tab; diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index fddbd6a1..a3f62390 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -61,7 +61,7 @@ public enum Status { ERROR ("Error"); /** The status name. */ - private String name; + private final String name; /** * Constructor. diff --git a/src/itdelatrisu/opsu/downloads/Updater.java b/src/itdelatrisu/opsu/downloads/Updater.java index 366fb623..8b7166e9 100644 --- a/src/itdelatrisu/opsu/downloads/Updater.java +++ b/src/itdelatrisu/opsu/downloads/Updater.java @@ -77,7 +77,7 @@ public String getDescription() { UPDATE_FINAL ("Update queued."); /** The status description. */ - private String description; + private final String description; /** * Constructor. diff --git a/src/itdelatrisu/opsu/replay/LifeFrame.java b/src/itdelatrisu/opsu/replay/LifeFrame.java index 045075f9..de7daea4 100644 --- a/src/itdelatrisu/opsu/replay/LifeFrame.java +++ b/src/itdelatrisu/opsu/replay/LifeFrame.java @@ -25,10 +25,10 @@ */ public class LifeFrame { /** Time. */ - private int time; + private final int time; /** Percentage. */ - private float percentage; + private final float percentage; /** * Constructor. diff --git a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java index ec2fad6d..4b17adac 100644 --- a/src/itdelatrisu/opsu/replay/PlaybackSpeed.java +++ b/src/itdelatrisu/opsu/replay/PlaybackSpeed.java @@ -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(); diff --git a/src/itdelatrisu/opsu/replay/ReplayFrame.java b/src/itdelatrisu/opsu/replay/ReplayFrame.java index 9f8e9f98..8f6e5095 100644 --- a/src/itdelatrisu/opsu/replay/ReplayFrame.java +++ b/src/itdelatrisu/opsu/replay/ReplayFrame.java @@ -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. diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index 1892e0e7..1f21300e 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -256,7 +256,7 @@ public void scroll(GameContainer container, StateBasedGame game, int newValue) { }; /** The buttons in the state. */ - private Button[] buttons; + private final Button[] buttons; /** The associated MenuButton objects. */ private MenuButton[] menuButtons; @@ -548,10 +548,10 @@ public void click(GameContainer container, StateBasedGame game) { }; /** 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. @@ -594,7 +594,7 @@ public void click(GameContainer container, StateBasedGame game) {} private GameContainer container; private StateBasedGame game; private Input input; - private int state; + private final int state; public ButtonMenu(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 50f37fa9..219f61a3 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -162,10 +162,10 @@ private enum Page { RESET, CURRENT, PREVIOUS, NEXT }; /** Search query helper class. */ private class SearchQuery implements Runnable { /** The search query. */ - private String query; + private final String query; /** The download server. */ - private DownloadServer server; + private final DownloadServer server; /** Whether the query was interrupted. */ private boolean interrupted = false; @@ -246,7 +246,7 @@ else if (totalResults == 0 || resultList.length == 0) private GameContainer container; private StateBasedGame game; private Input input; - private int state; + private final int state; public DownloadsMenu(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 823f359d..51c5780c 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -240,7 +240,7 @@ public enum Restart { private GameContainer container; private StateBasedGame game; private Input input; - private int state; + private final int state; public Game(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index 7a01c492..84d61535 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -62,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) { diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 4fcc18ec..14afd314 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -68,7 +68,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) { diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 83d4951c..11f2a33a 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -122,7 +122,7 @@ private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING } private GameContainer container; private StateBasedGame game; private Input input; - private int state; + private final int state; public MainMenu(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 60a48ade..ea9bd251 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -114,10 +114,10 @@ private enum OptionTab { 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; @@ -167,7 +167,7 @@ private enum OptionTab { private StateBasedGame game; private Input input; private Graphics g; - private int state; + private final int state; public OptionsMenu(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 44cd4834..01d1ad9e 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -209,7 +209,7 @@ public SongNode(BeatmapSetNode node, int index) { private GameContainer container; private StateBasedGame game; private Input input; - private int state; + private final int state; public SongMenu(int state) { this.state = state; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index d69b5b8a..c97ab6b3 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -67,7 +67,7 @@ public class Splash extends BasicGameState { private AnimatedValue logoAlpha; // game-related variables - private int state; + private final int state; private GameContainer container; private boolean init = false; From ae149a61f68ed01b0ea002df63f838ed4a679310 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 21:24:58 -0500 Subject: [PATCH 07/40] HP is now drained while holding sliders. (fixes #103) Slider ticks now give HP, and the HP for SLIDER30 results is doubled. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 3 ++- src/itdelatrisu/opsu/objects/Slider.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index a9473715..b72810c2 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -1188,7 +1188,7 @@ public void sliderTickResult(int time, int result, float x, float y, HitObject h switch (result) { case HIT_SLIDER30: hitValue = 30; - changeHealth(1f); + changeHealth(2f); SoundController.playHitSound( hitObject.getEdgeHitSoundType(repeat), hitObject.getSampleSet(repeat), @@ -1196,6 +1196,7 @@ public void sliderTickResult(int time, int result, float x, float y, HitObject h break; case HIT_SLIDER10: hitValue = 10; + changeHealth(1f); SoundController.playHitSound(HitSound.SLIDERTICK); break; case HIT_MISS: diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 2fe24b73..65f513f5 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -485,7 +485,6 @@ else if (GameMod.RELAX.isActive() && trackPosition >= time) 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) { From 130f9bf445dadb483f97b9e2e61e789ce5d56975 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 22:06:27 -0500 Subject: [PATCH 08/40] Game ranking screen fixes. - Show retry button only when appropriate (e.g. not when viewing replays or when playing with "Auto" enabled). - Watching a (fake) "replay" with "Auto" enabled will actually just call the "retry" code now. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 14 ++++++++++---- src/itdelatrisu/opsu/states/Game.java | 3 +++ src/itdelatrisu/opsu/states/GameRanking.java | 12 ++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index b72810c2..0b67bb71 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -324,7 +324,7 @@ public HitObjectResult(int time, int result, float x, float y, Color color, 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; @@ -337,7 +337,7 @@ public HitObjectResult(int time, int result, float x, float y, Color color, public GameData(int width, int height) { this.width = width; this.height = height; - this.gameplay = true; + this.isGameplay = true; clear(); } @@ -353,7 +353,7 @@ public GameData(int width, int height) { 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; @@ -1468,7 +1468,13 @@ public Replay getReplay(ReplayFrame[] frames, Beatmap beatmap) { * 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. + * @return 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. diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 51c5780c..5d44d7a2 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -738,6 +738,7 @@ else if (replayFrames != null) { r.save(); } ScoreData score = data.getScoreData(beatmap); + data.setGameplay(!isReplay); // add score to database if (!unranked && !isReplay) @@ -1111,6 +1112,8 @@ public void enter(GameContainer container, StateBasedGame game) GameMod.loadModState(replay.mods); } + data.setGameplay(true); + // check restart state if (restart == Restart.NEW) { // new game diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 14afd314..b344bbd2 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -20,6 +20,7 @@ import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Utils; @@ -113,7 +114,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // buttons replayButton.draw(); - if (data.isGameplay()) + if (data.isGameplay() && !GameMod.AUTO.isActive()) retryButton.draw(); UI.getBackButton().draw(); @@ -175,7 +176,8 @@ public void mousePressed(int button, int x, int y) { // 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 void mousePressed(int button, int x, int y) { } // 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 void enter(GameContainer container, StateBasedGame game) } else { SoundController.playSound(SoundEffect.APPLAUSE); retryButton.resetHover(); - replayButton.setY(replayY); + replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY); } replayButton.resetHover(); } From ae5016f3ab77c7197a906f897dfde462e6ed8678 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 20 Aug 2015 22:58:45 -0500 Subject: [PATCH 09/40] Validate audio files before trying to load them. (part of #120) Also fixed an issue where songMenu.resetGameDataOnLoad() sometimes wasn't being called when it should be (e.g. after playing a beatmap, if you move to a different screen, your score won't appear in the song menu right away). It's now being called more often than necessary, but that should be fine. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 2 +- src/itdelatrisu/opsu/Opsu.java | 6 ++---- src/itdelatrisu/opsu/audio/MusicController.java | 11 ++++++++++- src/itdelatrisu/opsu/states/GameRanking.java | 7 +++---- src/itdelatrisu/opsu/states/SongMenu.java | 6 +++++- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 0b67bb71..5947ebfb 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -1472,7 +1472,7 @@ public Replay getReplay(ReplayFrame[] frames, Beatmap beatmap) { /** * Sets whether or not this object is used for gameplay. - * @return true if gameplay, false if score viewing + * @param gameplay true if gameplay, false if score viewing */ public void setGameplay(boolean gameplay) { this.isGameplay = gameplay; } diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 3b548389..b97166ea 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -201,12 +201,9 @@ public boolean closeRequested() { 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(); @@ -215,6 +212,7 @@ public boolean closeRequested() { } if (UI.getCursor().isSkinned()) UI.getCursor().reset(); + songMenu.resetGameDataOnLoad(); this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); return false; } diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index 695dbe28..4f1f04e9 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -22,6 +22,7 @@ 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.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 @@ private 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 static void play(final Beatmap beatmap, final boolean loop, final boolean trackLoader = new Thread() { @Override public void run() { - loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop); + loadTrack(audioFile, (preview) ? beatmap.previewTime : 0, loop); } }; trackLoader.start(); diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index b344bbd2..b449763d 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -243,11 +243,10 @@ public void leave(GameContainer container, StateBasedGame game) */ 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(); - } + songMenu.resetGameDataOnLoad(); if (UI.getCursor().isSkinned()) UI.getCursor().reset(); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 01d1ad9e..63b5f4ea 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -1318,8 +1318,12 @@ private void startGame() { return; SoundController.playSound(SoundEffect.MENUHIT); - MultiClip.destroyExtraClips(); Beatmap beatmap = MusicController.getBeatmap(); + if (focusNode == null || beatmap != focusNode.getBeatmapSet().get(focusNode.beatmapIndex)) { + 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); From 949b2c215c3bfd3f2dd64abd2b868fce64568dd8 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 21 Aug 2015 10:25:52 -0500 Subject: [PATCH 10/40] Added a WatchService to watch the beatmap directory tree for changes. Any CREATE and DELETE that occur in the song menu state will now show a notification and modify the behavior of the 'F5' key. Changes that occur in other states will force a reload upon entering the song menu. This is part of osu!, but as it's not incredibly helpful, I've left it disabled by default. It can be enabled in the options menu. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Container.java | 6 + src/itdelatrisu/opsu/Options.java | 9 +- .../opsu/beatmap/BeatmapParser.java | 10 + .../opsu/beatmap/BeatmapWatchService.java | 275 ++++++++++++++++++ src/itdelatrisu/opsu/states/OptionsMenu.java | 3 +- src/itdelatrisu/opsu/states/SongMenu.java | 126 +++++--- src/itdelatrisu/opsu/states/Splash.java | 18 +- src/itdelatrisu/opsu/ui/UI.java | 2 +- 8 files changed, 402 insertions(+), 47 deletions(-) create mode 100644 src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index 6fa3fc7e..e9ef3f5c 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -21,6 +21,7 @@ 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; @@ -136,6 +137,11 @@ private void close_sub() { // delete OpenGL objects involved in the Curve rendering CurveRenderState.shutdown(); + + // destroy watch service + if (!Options.isWatchServiceEnabled()) + BeatmapWatchService.destroy(); + BeatmapWatchService.removeListeners(); } @Override diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 9d824f26..e9fdbb56 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -456,7 +456,8 @@ public String getValueString() { }, 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); + 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 final String name; @@ -973,6 +974,12 @@ public static void setDisplayMode(Container app) { */ 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) diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index be729980..103ac6e2 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -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 static void parseAllFiles(File root) { // 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 static BeatmapSetNode parseDirectories(File[] dirs) { List cachedBeatmaps = new LinkedList(); // loaded from database List parsedBeatmaps = new LinkedList(); // loaded from parser + // watch service + BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null; + // parse directories BeatmapSetNode lastNode = null; for (File dir : dirs) { @@ -162,6 +170,8 @@ public boolean accept(File dir, String name) { if (!beatmaps.isEmpty()) { beatmaps.trimToSize(); allBeatmaps.add(beatmaps); + if (ws != null) + ws.registerAll(dir.toPath()); } // stop parsing files (interrupted) diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java new file mode 100644 index 00000000..a287e198 --- /dev/null +++ b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java @@ -0,0 +1,275 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.beatmap; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.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. */ + public void eventReceived(WatchEvent.Kind kind, Path child); + } + + /** The list of listeners. */ + private static final List listeners = new ArrayList(); + + /** + * 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 keys; + + /** The Executor. */ + private ExecutorService service; + + /** + * Creates the WatchService. + * @throws IOException if an I/O error occurs + */ + private BeatmapWatchService() throws IOException { + this.watcher = FileSystems.getDefault().newWatchService(); + this.keys = new ConcurrentHashMap(); + } + + /** + * 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() { + @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 WatchEvent cast(WatchEvent event) { + return (WatchEvent) 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; + + 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 ev = cast(event); + Path name = ev.context(); + Path child = dir.resolve(name); + //System.out.printf("%s: %s\n", kind.name(), child); + + // fire listeners + 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 + } + } + } +} diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index ea9bd251..3792d8b4 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -97,7 +97,8 @@ private enum OptionTab { GameOption.FIXED_OD, GameOption.CHECKPOINT, GameOption.REPLAY_SEEKING, - GameOption.DISABLE_UPDATER + GameOption.DISABLE_UPDATER, + GameOption.ENABLE_WATCH_SERVICE }); /** Total number of tabs. */ diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 63b5f4ea..2fd00018 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -36,6 +36,8 @@ 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.db.BeatmapDB; import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; @@ -47,6 +49,9 @@ 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; @@ -205,6 +210,9 @@ public SongNode(BeatmapSetNode node, int index) { /** 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; + // game-related variables private GameContainer container; private StateBasedGame game; @@ -283,6 +291,19 @@ public void init(GameContainer container, StateBasedGame game) 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."); + } + } + }); } @Override @@ -775,8 +796,12 @@ public void keyPressed(int key, char c) { 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) @@ -949,8 +974,12 @@ public void enter(GameContainer container, StateBasedGame game) // reset song stack randomStack = new Stack(); + // 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 @@ -1069,44 +1098,7 @@ else if (startNode.next != null) } break; case RELOAD: // reload beatmaps - // reset state and node references - MusicController.reset(); - startNode = focusNode = null; - scoreMap = null; - focusScores = null; - oldFocusNode = null; - randomStack = new Stack(); - songInfo = null; - hoverOffset.setTime(0); - hoverIndex = -1; - 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; @@ -1310,6 +1302,58 @@ private ScoreData[] getScoreDataForNode(BeatmapSetNode node, boolean setTimeSinc 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(); + songInfo = null; + hoverOffset.setTime(0); + hoverIndex = -1; + search.setText(""); + searchTimer = SEARCH_DELAY; + searchTransitionTimer = SEARCH_TRANSITION_TIME; + searchResultString = 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(); + } + /** * Starts the game. */ diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index c97ab6b3..b5bed1ee 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapSetList; +import itdelatrisu.opsu.beatmap.BeatmapWatchService; import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimatedValue; @@ -63,6 +64,9 @@ 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; @@ -84,6 +88,9 @@ public void init(GameContainer container, StateBasedGame game) 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); @@ -108,13 +115,18 @@ public void update(GameContainer container, StateBasedGame game, int delta) // 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; diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 64471614..b855aa8b 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -64,7 +64,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; From b83d6be5fd7a8f3c68b5fed7ca9e991ce737e9ee Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 23 Aug 2015 20:41:09 -0500 Subject: [PATCH 11/40] Clarified GameImage "skinnable" field as "beatmap-skinnable". Renamed some methods and modified comments to avoid this confusion, as game skins were implemented much later than beatmap skins. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameData.java | 6 +-- src/itdelatrisu/opsu/GameImage.java | 46 +++++++++---------- src/itdelatrisu/opsu/Opsu.java | 2 +- src/itdelatrisu/opsu/Utils.java | 2 +- src/itdelatrisu/opsu/objects/Slider.java | 4 +- src/itdelatrisu/opsu/states/Game.java | 4 +- .../opsu/states/GamePauseMenu.java | 12 ++--- src/itdelatrisu/opsu/states/GameRanking.java | 2 +- src/itdelatrisu/opsu/states/SongMenu.java | 6 +-- src/itdelatrisu/opsu/ui/Cursor.java | 20 ++++---- 10 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 5947ebfb..b2a065ca 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -132,7 +132,7 @@ public Image getMenuImage() { return menuImage; Image img = getSmallImage(); - if (!small.hasSkinImage()) // save default image only + if (!small.hasBeatmapSkinImage()) // save default image only this.menuImage = img; return img; } @@ -408,8 +408,8 @@ public void loadImages() { // 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() }; diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 8d0dda19..1789636d 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -37,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"), @@ -496,7 +496,7 @@ private static String[] getSuffixes() { } /** - * 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 '|') */ @@ -505,7 +505,7 @@ private static String[] getSuffixes() { } /** - * 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 '|') @@ -519,7 +519,7 @@ private static String[] getSuffixes() { * 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 skinnable 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) { @@ -530,10 +530,10 @@ private static String[] getSuffixes() { } /** - * Returns whether or not the image is skinnable. + * Returns whether or not the image is beatmap-skinnable. * @return true if skinnable */ - public boolean isSkinnable() { return skinnable; } + public boolean isBeatmapSkinnable() { return skinnable; } /** * Returns whether or not to preload the image when the program starts. @@ -543,7 +543,7 @@ private static String[] getSuffixes() { /** * 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(); @@ -564,7 +564,7 @@ public Animation getAnimation(int duration){ /** * 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(); @@ -573,7 +573,7 @@ public Image[] getImages() { /** * 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) { @@ -585,7 +585,7 @@ public void setImage(Image img) { /** * 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 */ @@ -628,17 +628,17 @@ public void setDefaultImage() { } /** - * 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()) @@ -717,21 +717,21 @@ private Image loadImageSingle(File dir) { } /** - * Returns whether a skin image is currently loaded. - * @return true if skin image exists + * Returns whether a beatmap skin image is currently loaded. + * @return true if a beatmap skin image exists */ - public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); } + public boolean hasBeatmapSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); } /** - * Returns whether skin images are currently loaded. - * @return true if any skin image exists + * Returns whether beatmap skin images are currently loaded. + * @return true if any beatmap skin image exists */ - public boolean hasSkinImages() { return (skinImages != null); } + public boolean hasBeatmapSkinImages() { return (skinImages != null); } /** - * Destroys the associated skin image(s), if any. + * Destroys the associated beatmap skin image(s), if any. */ - public void destroySkinImage() { + public void destroyBeatmapSkinImage() { if (skinImage == null && skinImages == null) return; try { @@ -748,7 +748,7 @@ public void destroySkinImage() { 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); } } diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index b97166ea..49cf9f45 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -210,7 +210,7 @@ public boolean closeRequested() { } 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)); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 6126dcc0..b5e811c4 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -427,7 +427,7 @@ public static List wrap(String text, org.newdawn.slick.Font font, int wi if (str.length() != 0) list.add(str); return list; - } + } /** * Returns a the contents of a URL as a string. diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 65f513f5..160f74aa 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -128,8 +128,8 @@ public static void init(GameContainer container, float circleSize, Beatmap beatm 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() }; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 5d44d7a2..985406f4 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -1424,9 +1424,9 @@ private void loadImages() { // 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); } } diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index 84d61535..35f943f8 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -87,10 +87,10 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // 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); @@ -134,7 +134,7 @@ else if (key == Options.getGameKeyRight()) 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 { @@ -188,7 +188,7 @@ public void mousePressed(int button, int x, int y) { 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)); } diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index b449763d..8a9c9d18 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -247,7 +247,7 @@ private void returnToSongMenu() { if (data.isGameplay()) songMenu.resetTrackOnLoad(); songMenu.resetGameDataOnLoad(); - if (UI.getCursor().isSkinned()) + if (UI.getCursor().isBeatmapSkinned()) UI.getCursor().reset(); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 2fd00018..be7ea09b 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -1006,8 +1006,8 @@ else if (MusicController.isPaused()) // destroy skin images, if any for (GameImage img : GameImage.values()) { - if (img.isSkinnable()) - img.destroySkinImage(); + if (img.isBeatmapSkinnable()) + img.destroyBeatmapSkinImage(); } // reload scores @@ -1333,7 +1333,7 @@ public void run() { if (fullReload) { // clear the beatmap cache BeatmapDB.clearDatabase(); - + // invoke unpacker OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir); } diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index e9eea817..cc9149a3 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -107,11 +107,11 @@ public void draw() { public void draw(int mouseX, int mouseY, boolean mousePressed) { // determine correct cursor image Image cursor = null, cursorMiddle = null, cursorTrail = null; - boolean skinned = GameImage.CURSOR.hasSkinImage(); + boolean skinned = GameImage.CURSOR.hasBeatmapSkinImage(); boolean newStyle, hasMiddle; if (skinned) { 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) { @@ -253,13 +253,13 @@ public void update(int delta) { } /** - * 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(); @@ -282,10 +282,10 @@ public void resetLocations() { /** * 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()); } /** From 5efb61d3bbdbb8608b9bbd755acbdfcc48c25bcd Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 23 Aug 2015 21:16:28 -0500 Subject: [PATCH 12/40] Fixed bug where skinned old-style cursors were ignored. (part of #120) Previously, enabling old-style cursors would try to load files "cursor2" and "cursortrail2". Now the actual files are loaded, and the -2 images are only used when no game skin image is provided. Added a hasGameSkinImage() method in GameImage to check if the "default" image is skinned (by a game skin). Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameImage.java | 40 +++++++++++++++++------ src/itdelatrisu/opsu/states/MainMenu.java | 2 +- src/itdelatrisu/opsu/ui/Cursor.java | 17 +++++----- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 1789636d..798dc090 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -371,7 +371,7 @@ protected Image process_sub(Image img, int w, int h) { * Whether or not the image is skinnable by a beatmap. * These images are typically related to gameplay. */ - private final boolean skinnable; + private final boolean beatmapSkinnable; /** Whether or not to preload the image when the program starts. */ private final boolean preload; @@ -382,6 +382,9 @@ protected Image process_sub(Image img, int w, int h) { /** 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; @@ -429,6 +432,7 @@ public static void clearReferences() { for (GameImage img : GameImage.values()) { img.defaultImage = img.skinImage = null; img.defaultImages = img.skinImages = null; + img.isSkinned = false; } } @@ -519,21 +523,21 @@ private static String[] getSuffixes() { * 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 beatmap-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 beatmap-skinnable. - * @return true if skinnable + * @return true if beatmap-skinnable */ - public boolean isBeatmapSkinnable() { return skinnable; } + public boolean isBeatmapSkinnable() { return beatmapSkinnable; } /** * Returns whether or not to preload the image when the program starts. @@ -610,16 +614,26 @@ public void setDefaultImage() { // 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; } @@ -716,6 +730,12 @@ private Image loadImageSingle(File dir) { return null; } + /** + * 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 hasGameSkinImage() { return isSkinned; } + /** * Returns whether a beatmap skin image is currently loaded. * @return true if a beatmap skin image exists diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 11f2a33a..6b86cdcf 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -220,7 +220,7 @@ public void init(GameContainer container, StateBasedGame game) 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(300, 0f, 1f, AnimationEquation.LINEAR); + logoButtonAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR); reset(); } diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index cc9149a3..506148b1 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -107,27 +107,24 @@ public void draw() { public void draw(int mouseX, int mouseY, boolean mousePressed) { // determine correct cursor image Image cursor = null, cursorMiddle = null, cursorTrail = null; - boolean skinned = GameImage.CURSOR.hasBeatmapSkinImage(); + 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.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; - float FPSmod = Math.max(container.getFPS(), 1) / 60f; - Skin skin = Options.getSkin(); - // scale cursor float cursorScale = Options.getCursorScale(); if (mousePressed && skin.isCursorExpanded()) @@ -138,6 +135,8 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { } // 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) { From 017150233b5feab9966d7b5c1581e03e1a56b916 Mon Sep 17 00:00:00 2001 From: Szunti Date: Wed, 12 Aug 2015 15:41:11 +0200 Subject: [PATCH 13/40] Rewrote shaders for GLSL 110 (openGL2.0). --- .../opsu/objects/curves/Curve.java | 4 ++-- .../opsu/render/CurveRenderState.java | 21 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index fad50877..682fda94 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -87,12 +87,12 @@ public static void init(int width, int height, float circleSize, Color borderCol Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; + mmsliderSupported = capabilities.GL_EXT_framebuffer_object; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) - Log.warn("New slider style requires FBO support and OpenGL 3.0."); + Log.warn("New slider style requires FBO support."); } } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 3696a056..dffd91f7 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -33,7 +33,6 @@ 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; @@ -396,7 +395,7 @@ public void initGradient() { 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); } } @@ -409,12 +408,12 @@ public void initShaderProgram() { program = GL20.glCreateProgram(); int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER); int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER); - GL20.glShaderSource(vtxShdr, "#version 130\n" + GL20.glShaderSource(vtxShdr, "#version 110\n" + "\n" - + "in vec4 in_position;\n" - + "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" @@ -426,22 +425,21 @@ public void initShaderProgram() { String error = GL20.glGetShaderInfoLog(vtxShdr, 1024); Log.error("Vertex Shader compilation failed.", new Exception(error)); } - GL20.glShaderSource(frgShdr, "#version 130\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" - + "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); @@ -451,7 +449,6 @@ public void initShaderProgram() { } GL20.glAttachShader(program, vtxShdr); GL20.glAttachShader(program, frgShdr); - GL30.glBindFragDataLocation(program, 0, "out_colour"); GL20.glLinkProgram(program); res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS); if (res != GL11.GL_TRUE) { From 20d40dd520c0043a23f0bb4a743d03278aec3cfe Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Wed, 26 Aug 2015 09:29:28 -0500 Subject: [PATCH 14/40] Better handling of misnamed MP3/OGG files. (part of #120) This also catches more MP3 loading errors that could occur and properly cleans up resources. Signed-off-by: Jeffrey Han --- .../newdawn/slick/openal/Mp3InputStream.java | 7 +++- .../slick/openal/OpenALStreamPlayer.java | 32 +++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/org/newdawn/slick/openal/Mp3InputStream.java b/src/org/newdawn/slick/openal/Mp3InputStream.java index 8d8cb6df..19dd105f 100644 --- a/src/org/newdawn/slick/openal/Mp3InputStream.java +++ b/src/org/newdawn/slick/openal/Mp3InputStream.java @@ -76,8 +76,9 @@ public class Mp3InputStream extends InputStream implements AudioInputStream { /** * Create a new stream to decode MP3 data. * @param input the input stream from which to read the MP3 file + * @throws IOException failure to read the header from the input stream */ - public Mp3InputStream(InputStream input) { + public Mp3InputStream(InputStream input) throws IOException { decoder = new Decoder(); bitstream = new Bitstream(input); try { @@ -85,6 +86,10 @@ public Mp3InputStream(InputStream input) { } catch (BitstreamException e) { Log.error(e); } + if (header == null) { + close(); + throw new IOException("Failed to read header from MP3 input stream."); + } channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2; sampleRate = header.frequency(); diff --git a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java index a6e6c3f4..cc13a3e5 100644 --- a/src/org/newdawn/slick/openal/OpenALStreamPlayer.java +++ b/src/org/newdawn/slick/openal/OpenALStreamPlayer.java @@ -152,16 +152,30 @@ private void initStreams() throws IOException { if (url != null) { audio = new OggInputStream(url.openStream()); } else { - if (ref.toLowerCase().endsWith(".mp3")) - audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref)); - else - audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref)); - - if (audio.getRate() == 0 && audio.getChannels() == 0) { - if (ref.toLowerCase().endsWith(".mp3")) - audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref)); - else + if (ref.toLowerCase().endsWith(".mp3")) { + try { audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref)); + } catch (IOException e) { + // invalid MP3: check if file is actually OGG + try { + audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref)); + } catch (IOException e1) { + throw e; // invalid OGG: re-throw original MP3 exception + } + if (audio.getRate() == 0 && audio.getChannels() == 0) + throw e; // likely not OGG: re-throw original MP3 exception + } + } else { + audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref)); + if (audio.getRate() == 0 && audio.getChannels() == 0) { + // invalid OGG: check if file is actually MP3 + AudioInputStream audioOGG = audio; + try { + audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref)); + } catch (IOException e) { + audio = audioOGG; // invalid MP3: keep OGG stream + } + } } } From dea0104c4d39ac9f84f0edd3098c8ef3ece947d7 Mon Sep 17 00:00:00 2001 From: Lemmmy Date: Tue, 25 Aug 2015 23:00:34 +0100 Subject: [PATCH 15/40] Added the Gradle build system and added a new native loader --- .gitignore | 7 +- build.gradle | 99 +++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52141 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++++++++++ gradlew.bat | 90 ++++++++++ pom.xml | 36 ++-- res/version | 4 +- settings.gradle | 1 + src/itdelatrisu/opsu/NativeLoader.java | 108 ++++++++++++ src/itdelatrisu/opsu/Opsu.java | 60 ++++--- src/itdelatrisu/opsu/Utils.java | 27 +-- .../objects/curves/CircumscribedCircle.java | 3 +- .../opsu/objects/curves/Curve.java | 8 - .../curves/EqualDistanceMultiCurve.java | 7 +- src/org/newdawn/slick/Input.java | 4 +- 16 files changed, 546 insertions(+), 78 deletions(-) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/itdelatrisu/opsu/NativeLoader.java diff --git a/.gitignore b/.gitignore index ab225628..4ba1441d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,11 @@ .idea/ *.iml *.iws +*.ipr + +# Gradle +.gradle +build/ Thumbs.db -/target +/target \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..22940162 --- /dev/null +++ b/build.gradle @@ -0,0 +1,99 @@ +apply plugin: 'java' +apply plugin: 'maven' +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: 'application' + +import org.apache.tools.ant.filters.* + +group = 'itdelatrisu' +version = '0.10.1' + +mainClassName = 'itdelatrisu.opsu.Opsu' +buildDir = new File(rootProject.projectDir, "build/") + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +sourceSets { + main { + java { + srcDir 'src' + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + compile 'org.lwjgl.lwjgl:lwjgl:2.9.3' + compile 'org.slick2d:slick2d-core:1.0.1' + compile 'org.jcraft:jorbis:0.0.17' + compile 'net.lingala.zip4j:zip4j:1.3.2' + compile 'com.googlecode.soundlibs:jlayer:1.0.1-1' + compile 'com.googlecode.soundlibs:mp3spi:1.9.5-1' + compile 'com.googlecode.soundlibs:tritonus-share:0.3.7-2' + compile 'org.xerial:sqlite-jdbc:3.8.6' + compile 'org.json:json:20140107' + compile 'net.java.dev.jna:jna:4.1.0' + compile 'net.java.dev.jna:jna-platform:4.1.0' + compile 'org.apache.maven:maven-artifact:3.3.3' + compile 'org.apache.commons:commons-compress:1.9' + compile 'org.tukaani:xz:1.5' + compile 'com.github.jponge:lzma-java:1.3' + + +} + +def nativePlatforms = ['windows', 'linux', 'osx'] +nativePlatforms.each { platform -> //noinspection GroovyAssignabilityCheck + task "${platform}Natives" { + def outputDir = "${buildDir}/natives/" + inputs.files(configurations.compile) + outputs.dir(outputDir) + doLast { + copy { + def artifacts = configurations.compile.resolvedConfiguration.resolvedArtifacts + .findAll { it.classifier == "natives-$platform" } + artifacts.each { + from zipTree(it.file) + } + into outputDir + } + } + } +} + +processResources { + from 'res' + exclude '**/Thumbs.db' + + filesMatching('version') { + expand(version: project.version, timestamp: new Date().format("yyyy-MM-dd HH:mm")) + } +} + +task unpackNatives { + description "Copies native libraries to the build directory." + dependsOn nativePlatforms.collect { "${it}Natives" }.findAll { tasks[it] } +} + +jar { + manifest { + attributes 'Implementation-Title': 'opsu!', + 'Implementation-Version': version, + 'Main-Class': mainClassName + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + baseName = "opsu" + + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + exclude '**/Thumbs.db' +} + +run { + dependsOn 'unpackNatives' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..085a1cdc27db1185342f15a00441734e74fe3735 GIT binary patch literal 52141 zcmafaW0a=B^559DjdyI@wy|T|wr$(CJv+9!W822gY&N+!|K#4>Bz;ajPk*RBjZ;RV75EK-U36r8Y(BB5~-#>pF^k0$_Qx&35mhPenc zNjoahrs}{XFFPtR8Xs)MImdo3(FfIbReeZ6|xbrftHf0>dl5l+$$VLbG+m|;Uk##see6$CK4I^ ziDe}0)5eiLr!R5hk6u9aKT36^C>3`nJ0l07RQ1h438axccsJk z{kKyd*$G`m`zrtre~(!7|FcIGPiGfXTSX`PzlY^wY3ls9=iw>j>SAGP=VEDW=wk2m zk3%R`v9(7LLh{1^gpVy8R2tN#ZmfE#9!J?P7~nw1MnW^mRmsT;*cyVG*SVY6CqC3a zMccC8L%tQqGz+E@0i)gy&0g_7PV@3~zaE~h-2zQ|SdqjALBoQBT2pPYH^#-Hv8!mV z-r%F^bXb!hjQwm2^oEuNkVelqJLf029>h5N1XzEvYb=HA`@uO_*rgQZG`tKgMrKh~aq~ z6oX{k?;tz&tW3rPe+`Q8F5(m5dJHyv`VX0of2nf;*UaVsiMR!)TjB`jnN2)6z~3CK@xZ_0x>|31=5G$w!HcYiYRDdK3mtO1GgiFavDsn&1zs zF|lz}sx*wA(IJoVYnkC+jmhbirgPO_Y1{luB>!3Jr2eOB{X?e2Vh8>z7F^h$>GKmb z?mzET;(r({HD^;NNqbvUS$lhHSBHOWI#xwT0Y?b!TRic{ z>a%hUpta3P2TbRe_O;s5@KjZ#Dijg4f=MWJ9euZnmd$UCUNS4I#WDUT2{yhVWt#Ee z?upJB_de&7>FHYm0Y4DU!Kxso=?RabJ*qsZ2r4K8J#pQ)NF?zFqW#XG1fX6dFC}qh z3%NlVXc@Re3vkXi*-&m)~SYS?OA8J?ygD3?N}Pq zrt_G*8B7^(uS7$OrAFL5LvQdQE2o40(6v`se%21Njk4FoLV-L0BN%%w40%k6Z1ydO zb@T(MiW@?G-j^j5Ypl@!r`Vw&lkJtR3B#%N~=C z@>#A{z8xFL=2)?mzv;5#+HAFR7$3BMS-F=U<&^217zGkGFFvNktqX z3z79GH^!htJe$D-`^(+kG*);7qocnfnPr^ieTpx&P;Z$+{aC8@h<0DDPkVx`_J~J> zdvwQxbiM1B{J6_V?~PNusoB5B88S%q#$F@Fxs4&l==UW@>9w2iU?9qMOgQWCl@7C* zsbi$wiEQEnaum!v49B_|^IjgM-TqMW!vBhhvP?oB!Ll4o-j?u3JLLFHM4ZVfl9Y_L zAjz@_3X5r=uaf|nFreX#gCtWU44~pA!yjZNXiZkoHhE$l@=ZTuxcLh53KdMOfanVe zPEX(#8GM7#%2*2}5rrdBk8p#FmzpIC>%1I9!2nRakS|^I*QHbG_^4<=p)(YOKvsTp zE#DzUI>Y&g)4mMaU6Bhrm8rSC{F_4J9sJlF0S5y5_=^l!{?W_n&SPj&7!dEvLzNIRMZBYyYU@Qftts7Zr7r>W- zqqk46|LEF|&6bn#CE~yMbiF&vEoLUA(}WzwmXH_=<~|I(9~{AE$ireF7~XBqPV2)* zcqjOCdi&>tUEuq31s(|TFqx>Wuo(ooWO(sd!W~Hu@AXg=iQgq^O3Lv9xH$vx*vrgDAirQqs9_DLS1e45HcUPdEMziO?Mm1v!)n93L%REy=7 zUxcX!jo!vyl_l0)O(Y~OT``;8mB(tcf}`Rh^weqPnDVDe-ngsZ~C z`onh0WLdaShAAb-3b{hT5ej9a$POQ9;RlPy}IYzKyv+8-HzB7fV!6X@a_T61qZ zWqb&&ip*@{;D-1vR3F2Q&}%Q>TFH&2n?2w8u8g=Y{!|;>P%<@AlshvM;?r7I)yXG% z^IpXZ(~)V*j^~sOG#cWCa+b8LC1IgqFx+Mq$I`6VYGE#AUajA9^$u-{0X#4h49a77 zH>d>h3P@u!{7h2>1j+*KYSNrKE-Q(z`C;n9N>mfdrlWo$!dB35;G4eTWA}(aUj&mNyi-N+lcYGpA zt1<~&u`$tIurZ2-%Tzb1>mb(~B8;f^0?FoPVdJ`NCAOE~hjEPS) z&r7EY4JrG~azq$9$V*bhKxeC;tbBnMds48pDuRy=pHoP*GfkO(UI;rT;Lg9ZH;JU~ zO6gTCRuyEbZ97jQyV7hM!Nfwr=jKjYsR;u8o(`(;qJ(MVo(yA<3kJximtAJjOqT=3 z8Bv-^`)t{h)WUo&t3alsZRJXGPOk&eYf}k2JO!7Au8>cvdJ3wkFE3*WP!m_glB-Rt z!uB>HV9WGcR#2n(rm=s}ulY7tXn5hC#UrNob)-1gzn-KH8T?GEs+JBEU!~9Vg*f6x z_^m1N20Do}>UIURE4srAMM6fAdzygdCLwHe$>CsoWE;S2x@C=1PRwT438P@Vt(Nk` zF~yz7O0RCS!%hMmUSsKwK$)ZtC#wO|L4GjyC?|vzagOP#7;W3*;;k?pc!CA=_U8>% z%G^&5MtFhvKq}RcAl))WF8I#w$So?>+_VEdDm_2=l^K320w~Bn2}p+4zEOt#OjZ6b zxEYoTYzvs$%+ZYwj;mZ@fF42F1-Hb<&72{1J)(D~VyVpo4!dq259t-_Oo3Yg7*R`N zUg!js4NRyfMbS*NLEF}rGrlXz0lHz))&&+B#Tdo@wlh-Q8wr7~9)$;s9+yJH0|m=F zSD9mUW>@HLt}mhAApYrhdviKhW`BfNU3bPSz=hD+!q`t*IhG+Z4XK;_e#AkF5 z&(W7iUWF4PNQ+N!-b-^3B$J4KeA1}&ta@HK=o2khx!I&g#2Y&SWo-;|KXDw!Xb)mP z$`WzPA!F(h*E=QP4;hu7@8J&T|ZPQ2H({7Vau6&g;mer3q?1K!!^`|0ld26 zq|J&h7L-!zn!GnYhjp`c7rG>kd1Y%8yJE9M0-KtN=)8mXh45d&i*bEmm%(4~f&}q@ z1uq)^@SQ~L?aVCAU7ZYFEbZ<730{&m?Un?Q!pxI7DwA^*?HloDysHW{L!JY!oQ8WMK(vT z@fFakL6Ijo$S$GH;cfXcoNvwVc8R7bQnOX2N1s$2fbX@qzTv>748In?JUSk@41;-8 zBw`fUVf$Jxguy{m1t_Z&Q6N$Ww*L9e%6V*r3Yp8&jVpxyM+W?l0km=pwm21ch9}+q z$Z&eb9BARV1?HVgjAzhy);(y1l6)+YZ3+u%f@Y3stu5sSYjQl;3DsM719wz98y4uClWqeD>l(n@ce)pal~-24U~{wq!1Z_ z2`t+)Hjy@nlMYnUu@C`_kopLb7Qqp+6~P=36$O!d2oW=46CGG54Md`6LV3lnTwrBs z!PN}$Kd}EQs!G22mdAfFHuhft!}y;8%)h&@l7@DF0|oy?FR|*E&Zuf=e{8c&hTNu# z6{V#^p+GD@A_CBDV5sM%OA*NwX@k1t?2|)HIBeKk(9!eX#J>jN;)XQ%xq^qVe$I}& z{{cL^a}>@*ZD$Ve)sJVYC!nrAHpV~JiCH3b7AQfAsEfzB$?RgU%+x7jQ_5XQ8Gf*N`i<1mZE zg6*_1dR3B`$&9CxHzk{&&Hf1EHD*JJF2glyBR+hBPnwP@PurN`F80!5{J57z;=kAc za65ouFAve7QEOmfcKg*~HZ04-Ze%9f)9pgrVMf7jcVvOdS{rf+MOsayTFPT}3}YuH z$`%^f$}lBC8IGAma+=j9ruB&42ynhH!5)$xu`tu7idwGOr&t=)a=Y2Sib&Di`^u9X zHQ=liR@by^O`ph|A~{#yG3hHXkO>V|(%=lUmf3vnJa#c%Hc>UNDJZRJ91k%?wnCnF zLJzR5MXCp)Vwu3Ew{OKUb?PFEl6kBOqCd&Qa4q=QDD-N$;F36Z_%SG}6{h2GX6*57 zRQIbqtpQeEIc4v{OI+qzMg_lH=!~Ow%Xx9U+%r9jhMU=7$;L7yJt)q+CF#lHydiPP zQSD=AtDqdsr4G!m%%IauT@{MQs+n7zk)^q5!VQrp?mFajX%NQT#yG9%PTFP>QNtfTM%6+b^n%O`Bk74Ih| zb>Fh1ic{a<8g<{oJzd|@J)fVVqs&^DGPR-*mj?!Z?nr<f)C8^oI(N4feAst}o?y z-9Ne339xN7Lt|Tc50a48C*{21Ii$0a-fzG1KNwDxfO9wkvVTRuAaF41CyVgT?b46; zQvjU!6L0pZM%DH&;`u`!x+!;LaPBfT8{<_OsEC5>>MoJQ5L+#3cmoiH9=67gZa;rvlDJ7_(CYt3KSR$Q#UR*+0hyk z>Dkd2R$q~_^IL2^LtY|xNZR(XzMZJ_IFVeNSsy;CeEVH|xuS#>itf+~;XXYSZ9t%1moPWayiX=iA z!aU~)WgV!vNTU=N;SpQ((yz#I1R#rZ&q!XD=wdlJk4L&BRcq(>6asB_j$7NKLR%v; z9SSp$oL7O|kne`e@>Bdf7!sJ*MqAtBlyt9;OP3UU1O=u6eGnFWKT%2?VHlR86@ugy z>K)(@ICcok6NTTr-Jh7rk=3jr9`ao!tjF;r~GXtH~_&Wb9J^ zd%FYu_4^3_v&odTH~%mHE;RYmeo+x^tUrB>x}Is&K{f+57e-7Y%$|uN%mf;l5Za95 zvojcY`uSCH~kno zs4pMlci*Y>O_pcxZY#?gt1^b-;f(1l9}Ov7ZpHtxfbVMHbX;579A>16C&H5Q>pVpH5LLr<_=!7ZfX23b1L4^WhtD?5WG;^zM}T>FUHRJv zK~xq88?P);SX-DS*1LmYUkC?LNwPRXLYNoh0Qwj@mw9OP&u{w=bKPQ)_F0-ptGcL0 zhPPLKIbHq|SZ`@1@P5=G^_@i+U2QOp@MX#G9OI20NzJm60^OE;^n?A8CH+XMS&3ek zP#E7Y==p;4UucIV{^B`LaH~>g6WqcfeuB#1&=l!@L=UMoQ0$U*q|y(}M(Y&P$Xs&| zJ&|dUymE?`x$DBj27PcDTJJn0`H8>7EPTV(nLEIsO&9Cw1Dc&3(&XFt9FTc{-_(F+ z-}h1wWjyG5(ihWu_3qwi; zAccCjB3fJjK`p=0VQo!nPkr0fT|FG;gbH}|1p`U>guv9M8g2phJBkPC`}ISoje6+? zvX|r5a%Y-@WjDM1&-dIH2XM}4{{d&zAVJQEG9HB8FjX&+h*H=wK=xOgNh8WgwBxW+ z0=^CzC4|O_GM>^_%C!!2jd&x*n2--yT>PZJ`Mok6Vf4YFqYp@a%)W}F4^DpKh`Cr7 z{>Z7xw-4UfT@##s#6h%@4^s^7~$}p2$v^iR5uJljApd9%#>QuxvX+CSZv18MPeXPCizQ*bm);q zWhnVEeM}dlCQP*^8;Q7OM|SSgP+J;DQy|bBhuFwJ2y*^|dBwz96-H;~RNsc}#i= zwu`Tp4$bwRVb7dxGr_e1+bJEc=mxLxN_f>hwb#^|hNdewcYdqXPrOxDE;|mP#H|a% z{u8#Vn}zVP(yJ}+-dx;!8<1in=Q8KsU%Q5CFV%5mGi8L;)*m%Vs0+S`ZY(z7aZ$VCjp?{r>C<9@$zVN;LVhxzPEdDPdb8g<)pckA z?mG@Ri>ode(r|hjNwV#*{!B^l2KO@4A+!X;#PW#?v2U!ydYIFHiXC3>i2k7{VTfji>h z8-(^;x!>f)Qh$mlD-z^1Nxu})XPbN=AUsb%qhmTKjd=1BjKr(L9gb1w4Y8p+duWfS zU>%C>*lCR@+(ku!(>_SA6=4CeM|$k4-zv|3!wHy+H&Oc$SHr%QM(IaBS@#s}O?R7j ztiQ>j^{X)jmTPq-%fFDxtm%p|^*M;>yA;3WM(rLV_PiB~#Eaicp!*NztJNH;q5BW$ zqqlfSq@C0A7@#?oRbzrZTNgP1*TWt(1qHii6cp5U@n|vsFxJ|AG5;)3qdrM4JElmN z+$u4wOW7(>$mMVRVJHsR8roIe8Vif+ml3~-?mpRos62r0k#YjdjmK;rHd{;QxB?JV zyoIBkfqYBZ!LZDdOZArQlgXUGmbpe7B-y7MftT;>%aM1fy3?^CuC{al$2-tfcA?d) z<=t7}BWsxH3ElE^?E&|f{ODX&bs+Ax>axcdY5oQ`8hT)YfF%_1-|p*a9$R~C=-sT| zRA~-Q$_9|G(Pf9I+y!zc>fu)&JACoq&;PMB^E;gIj6WeU=I!+scfSr}I%oD1fh+AQ zB^Q^b@ti5`bhx+(5XG5*+##vV>30UCR>QLYxHYY~k!AR`O6O_a3&wuW61eyHaq;HL zqy@?I*fmB)XY;Z@RH^IR|6m1nwWv>PDONtZV-{3@RkM_JcroRNLTM9?=CI}l%p86A zdxv|{zFWNI;L8K9hFSxD+`-pwvnyS|O?{H-rg6dPH<3oXgF0vU5;~yXtBUXd>lDs~ zX!y3-Pr9l;1Q^Z<15_k1kg|fR%aJKzwkIyED%CdxoXql=^QB;^*=2nVfi{w?0c@Dj z_MQEYjDpf^`%)$|4h>XnnKw05e5p4Jy69{uJ5p|PzY+S?FF~KWAd0$W<`;?=M+^d zhH&>)@D9v1JH2DP?tsjABL+OLE2@IB)sa@R!iKTz4AHYhMiArm)d-*zitT+1e4=B( zUpObeG_s*FMg$#?Kn4%GKd{(2HnXx*@phT7rEV?dhE>LGR3!C9!M>3DgjkVR>W)p3 zCD0L3Ex5-#aJQS6lJXP9_VsQaki5#jx}+mM1`#(C8ga~rPL{2Z;^^b+0{X)_618Sw z0y6LTkk;)quIAYpPY{)fHJLk?)(vxt?roO24{C!ck}A)_$gGS>g!V^@`F#wg+%Cok zzt6hJE|ESs@S^oHMp3H?3SzqBh4AN(5SGi#(HCarl^(Jli#(%PaSP9sPJ-9plwZv{ z1lkTGk4UAXYP^>V+4;nQ4A~n-<+1N)1lPzXIbG{Q;e3~T_=Trak{WyjW+n!zhT*%)q?gx zTl4(Gf6Y|ALS!H$8O?=}AlN=^3yZCTX@)9g5b_fif_E{lWS~0t`KpH8kkSnWWz+G1 zjFrz}gTnQ2k-`oag*031Nj7=MZfP}gvrNvv_crWzf9Cdzv^LyBeEyF2#hGg8_C8jW)NCAhsm2W_P21DeX7x$4EDD){~vBiLoby=d+&(;_f(?PMfamC zI_z%>Nq-rC%#z#1UC49j4@m63@_7LWD$ze=1%GPh`%@PB7yGH6Zh=1#L%&%hU7z%Y zs!IN(ef@!+|1YR28@#kw^XR= zxB$*nNZm7Y@L0&IlmoN}kEI?dBee+z+!MWCy+e4P4MYpOgr}2Q(wnR1ZiA>5_P*Cg zB4BMlcx?(v*+V3O+p~Buk;wIN6v!Ut?gYpl+KFu~elf}{E4`9+lcR0k$bC>+I zWxO5jD8sYPbMS)4c3i2UojI4T7uzE*Zz;POw{0d0`*iHJ%(Pb=sa^pV{t_JtHoPeC zX+t_k*=D%+Sv#+5CeoRfI)G`T90~AE@K9RaFR%8*w#*x9>H$ahFd>PUg_zP`VVPSR zr#Rb;I--8Rq;eTBju;dx2cmZ9Al>aiDY z#7(4S(A#aRvl7jm78sQ+O^S5eUS8|W%5@Pt9fm?J=r`~=l-gdv(LB~C-Gi#srwEDQ z4cCvA*XiRj9VDR6Ccy2k(Nvxic;~%YrfNeWl$cJpa%WO_4k?wxKZ{&`V#!&#jV@x+ z7!!YxOskc;cAF~`&aRWp8E)fnELtvb3-eHkeBPb~lR&iH=lZd^ZB(T6jDg5PnkJQFu9? z+24ww5L%opvEkE$LUHkZDd0ljo!W}0clObhAz`cPFx2)X3Sk91#yLL}N6AE0_O`l| z7ZhaKuAi7$?8uuZAFL(G0x3wE<-~^neGm=*HgJa(((J;yQI$NB)J;i0?vr`M1v+R? zd+{rD^zK}0Gi!2lXo0P+jVQ$HNYn^sRMONYVZPPT@enUb1pHHYgZMo5GN~SIz*;gv z1H<4(%53!6$4+VX_@Kp!>A9wwo{(KdWx)ja>x3&4=H(Urbn?0Vh}W3%ly5SgJ<+X5?N7-B=byoKyICr>3 zIFXe;chMk7-cak~YKL8Bf>VbZbX{5L9ygP_XS?oByNL*zmp8&n9{D42I^=W=TTM4X zwb_0axNK?kQ;)QUg?4FvxxV7L@sndJL0O12M6TMorI&cAL%Q464id6?Tbd_H!;=SRW9w2M*wc00yKVFslv|WN( zY7=Yikt+VY@DpzKq7@z_bVqr7D5B3xRbMrU5IO7;~w2nNyP7J_Gp>>7z?3!#uT4%-~h6)Ee1H z&^g}vZ{g}DIs@FDzE$QG_smSuEyso@I#ID3-kkYXR=nYuaa0{%;$WzZC@j)MDi+jC z!8KC;1mGCHGKr>dR;3;eDyp^0%DH`1?c7JcsCx$=m(cs^4G& zl@Fi8z|>J`^Z-faK{mhsK|;m%9?luacM+~uhN@<20dfp4ZN@qsi%gM67zZ`OHw=PE zr95O@U(HheB7OBYtyF=*Z5V&m?WDvIQ`edwpnT?bV`boB z!wPf&-@7 z0SoTB^Cy>rDHm%^b0cv@xBO%02~^=M79S}TG8cbVhj72!yN_87}iA1;J$_xTb+Zi@76a{<{OP0h&*Yx`U+mkA#x3YQ} zPmJsUz}U0r?foPOWd5JFI_hs_%wHNa_@)?(QJXg>@=W_S23#0{chEio`80k%1S?FWp1U;4#$xlI-5%PEzJcm zxjp$&(9f2xEx!&CyZZw|PGx&4$gQbVM|<2J&H7rpu;@Mc$YmF9sz}-k0QZ!YT$DUw z_I=P(NWFl!G-}aofV?5egW%oyhhdVp^TZH%Q4 zA2gia^vW{}T19^8q9&jtsgGO4R70}XzC-x?W0dBo+P+J8ik=6}CdPUq-VxQ#u4JVJ zo7bigUNyEcjG432-Epy)Rp_WDgwjoYP%W|&U~Gq-r`XK=jsnWGmXW6F}c7eg;$PHh>KZ@{cbTI<`ZP>s(M@zy=aHMA2nb(L0COlVcl8UXK+6`@Di+Wai;lJf^7s6V%NkKcad zDYY%2utqcw#CJFT9*V9U_{DyP&VYb)(6y`Z%Rq& z!PTtuI#psBgLPoNu{xvs^y26`oY;p!fE=bJW!cP^T>bUE*UKBV5Bd%!U{Q5{bKwN> zv)pn@Oc{6RyIS>!@Yvkv+hVLe+bmQ6fY2L}tT)Vbewg8`A`PFYyP+@QmL?b{RED;; zR6fwAAD}Ogejah(58bv{VG&WJhll7X-hjO9dK`8m5uFvthD1+FkJtT_>*{yKA(lXx zKucHMz#F_G)yTJw!)I3XQ7^9ydSlr9D)z?e*jKYE?xTKjR|ci30McU^4unzPsHGKN zMqwGd{W_1_jBQ_oeU^4!Ih}*#AKF%7txXZ0GD}Jzcf+i*?WLAe6#R_R-bSr17K%If z8O2SwYwMviXiJ?+$% zse=E~rK*PH@1Md4PFP)t(NhV%L3$657FUMap?fugnm3|N z79w3|qE%QyqZB}2WG&yc>iOaweUb`5o5p9PgyjqdU*sXP=pi$-1$9fGXYgS2?grS6 zwo#J~)tUTa0tmGNk!bg*Pss&uthJDJ$n)EgE>GAWRGOXeygh;f@HGAi4f){s40n?k z=6IO?H1_Z9XGzBIYESSEPCJQrmru?=DG_47*>STd@5s;1Y|r*+(7s4|t+RHvH<2!K z%leY$lIA{>PD_0bptxA`NZx-L!v}T4JecK#92kr*swa}@IVsyk{x(S}eI)5X+uhpS z8x~2mNLf$>ZCBxqUo(>~Yy4Z3LMYahA0S6NW;rB%)9Q z8@37&h7T$v2%L|&#dkP}N$&Jn*Eqv81Y*#vDw~2rM7*&nWf&wHeAwyfdRd%`>ykby zC*W9p2UbiX>R^-!H-ubrR;5Z}og8xx!%)^&CMl(*!F%or1y&({bg?6((#og-6Hey&3th3S%!n3N|Z2ZCZHJxvQ9rt zv|N#i*1=qehIz_=n*TWC6x-ab)fGr8cu!oYV+N)}3M;H4%$jwO>L!e53sxmJC~;O; zhJw|^&=2p!b8uk{-M|Z*J9n0{(8^>P+Y7vlFLc8#weQMg2iB8MFCe-*^BJV6uVWjg zWZe{-t0f67J<|IIn4{wsKlG*Amy{-yOWMMW)g}rh>uEE;jbkS-om>uAjeTzCg51683UTmY4+yT zW!qe`?~F{~1Y>mPJ9M0hNRBW$%ZwOA-NdIeaE6_K z>y8D3tAD7{3FouIXX9_MbY;zq%Ce0}VmT;aO~=*Mk4mflb_i4CApxEtZ^TDNoOzy_ z-eIE(&n1Vz*j&(BjO*fVvSCozTJU4?tWC8m4=d|D{WV0k+0M2!F1=T}z7V4-JA*y( z!;H(sOBmg=%7p&LLf%z%>VgtdN6jl2y95aXY}v9U;m~YWx{2#lwLpEJWGgs`sE*15 zvK`DtH-Q^ix>9@qVG+d*-C{lYPBbts1|%3!CkLP1t4iz%LO-di4lY%{8>jd{turVrD*_lLv!ShQC~S#SXjCO?##c zh2aZKVAHDf1sQpZiH^C7NRu?44JuEp?%W4-?d;Dg z;`gKA9$oC{WlQuT?fex!ci3GJhU;1J!YLHbyh8B-jsZ~pl59LGannKg9}1qxlbOOq zaJhTl zEJ`2Xd_ffdK^EE1v>8kUZG`eMXw(9S+?Lxx#yTUo?WdV}5kjC|glSJqX zv8RO|m#Ed@hW=};Yfl&2_@11Xm}pz0*SRx%OH_NODo@>e$cMAv(0u`~Yo|qbQ~mzA zMKt^U+GIXKH^xuD9n}NfU|?ZTOSS>XJwlg`lYHgea)!ZR?m^=oj+qyKBd6SJvPZk* zwc-2$b%%V~k$5{=(rG!OcR{;u2V3um|C+oT5F?rt`CER|iU9-!_|GxMe^!f$d6*iz z{?~JnR84mS+!gFUxugG?g9uGFI(?Q0SADS8=n=#aCK^`6@rm4r=LJTBm;)cY zm_6c5!ni$SWFOuj36eKau>6=kl_p=-7>VL_fJuJZI}0=3kASf|t;B~;Mt(vuhCU+c zKCF@SJ5#1>8YLfe{pf?sH*v6C)rOvO1~%@+wN}#>dkcrLw8U@xAySc{UeaP?7^AQ5 zmThfw^(i@*GMlM!xf+dzhRtbo8#;6Ql_s$t15q%*KeCm3`JrXnU*T^hV-aGX)bmxF z;O%jGc{6G+$gZ$YvOM2bZ!?>X<^-D zbT+YCx722}NY88YhKnw?yjF1#vo1v+pjId;cdyT*SH@Bc>6(GV*IBkddKx%b?y!r6 z=?0sTwf`I_Jcm(J8D~X@ESiO`X&i53!9}5l}PXzSYf9 zd&=h`{8BP-R?E*Nk$yzSSFhz2uVerdhbcCWF{S7reTkzXB;U@{9`hvC0AscwoqqU( zKQavt5OPm9y1UpKL%O(SWSSX=eo2rky_8jJ-ew7>iw~T=Xrt3EEzc!slebwG)FrE> z>ASkjJk%#@%SFWs-X4)?TzbBtDuwF#;WVw}?(K`UYqm`3vKbFKuqQ8uL2Y5}%T0y5 zia#E?tyZgnuk$LD^ihIn(i~|1qs(%NpH844QX-2S5E)E7lSM=V56o>5vLB^7??Vy_ zgEIztL|85kDrYF(VUnJ$^5hA;|41_6k-zO#<7gdprPj;eY_Et)Wexf!udXbBkCUA)>vi1E!r2P_NTw6Vl6)%M!WiK+jLRKEoHMR zinUK!i4qkppano|OyK(5p(Dv3DW`<#wQVfDMXH~H(jJdP47Y~`% z#ue|pQaVSv^h#bToy|pL!rWz8FQ53tnbEQ5j#7op?#c#(tj@SM2X*uH!;v8KtS5Fo zW_HE8)jSL zYO}ii#_KujRL4G*5peU)-lDW0%E}!YwL#IKUX_1l9ijy~GTFhO?W^=vEBe?m+tvBe zLaGWcoKg==%dO#6R}`U0>M)2+{b*~uamlaUNN<_NVZTGY4-(ORqK6|HvKFMKwp6^L zR+MC^`6^|^=u^Do;wy8mUp^Oct9~=vQ74vfO-m&Q0#~-mkqkpw&dMkVJ(So<)tf3h z46~mW_3T@Mzh<2XZYO7@F4j|BbhhXjs*hayIjTKyGoYO}`jEFn^!4Y! zL30ubp4U(r>Nx&RhaJkGXuRe%%f%D;1-Zdw2-9^Mq{rP-ZNLMpi~m+v?L=sPSAGcc z{j+Y!3CVrm);@{ z;T?sp1|%lk1Q&`&bz+#6#NFT*?Zv3k!hEnMBRfN47vcpR20yJAYT(5MQ@k;5Xv@+J zLjFd{X_il?74aOAMr~6XUh7sT4^yyLl%D89Io`m5=qK_pimk+af+T^EF>Y)Z{^#b# zt%%Bj9>JW!1Zx_1exoU~obfxHy6mBA{V6E)12gLp-3=21=O82wENQ}H@{=SO89z&c*S8Veq8`a3l@EQO zqaNR8IItz4^}>9d+Oj%YUQlb;;*C0!iC&8gaiDJ)bqg(92<>RbXiqFI3t#jqI%3Y( zPop=j=AyLA?pMYaqp0eHbDViOWV-5IUVwx+Fl6M54*?i+MadJHIRjiQoUe?v-1XdQ z5S305nVbg|sy~qPr2C6}q!v)8E%$i~p5_jGPA0%3*F%>XW6g)@4-z73pVcvWs$J2m zpLeW4!!31%k#VUG76V__S**9oC{-&P6=^fGM$2q<+1eC}Fa2EB3^s{ru^hI}e^KPM zMyj;bLtsRex^QMcgF)1U0biJ|ATXX`YuhzWMwP73e0U?P=>L|R?+13$8(PB23(4Js zy@KS0vvS~rk*^07Bd4}^gpc|e5%248Mei_y^mrD;zUYniPazU>1Dun%bVQ0T7DNXr zMq4Y09V_Dr1OQ$ni)BSyXJZ+D7 zXHh02bToWd;4AlF-G`mk23kD=$9B)}*I@kF9$WcOHc%d6BdemN(!^z0B3rvR>NPQ? z+vv#Qa~Ht|BiTdcN;g6;eb6!Jso)MFD3{sf{T;!fM^OwcEtoJI#ta?+R>|R;Ty2E% zjF8@wgWC=}Kkv52c@8Psigo4#G#E?T(;i}rq+t}E(I(gAekZX;HbTR5ukI>8n5}oC zXXTcy>tC{sG$yFf?bIqBAK3C^X3OAY^Too{qI_uZga0cK4Z$g?Zu$#Eg|UEusQ)t% z{l}Zjf5OrK?wkKJ?X3yvfi{Nz4Jp5|WTnOlT{4sc3cH*z8xY(06G;n&C;_R!EYP+m z2jl$iTz%_W=^)Lhd_8hWvN4&HPyPTchm-PGl-v~>rM$b>?aX;E&%3$1EB7{?uznxn z%yp0FSFh(SyaNB@T`|yVbS!n-K0P|_9dl=oE`7b?oisW)if(`g73bkt^_NHNR_|XU z=g?00`gZRHZm+0B(KvZ0?&(n<#j!sFvr|;G2;8qWg3u%P;M1+UL!9nj)q!}cd}jxK zdw=K$?NuLj?2#YzTCEw1SfLr#3`3x(MB2F(j!6BMK!{jXF%qs;!bIFpar}^=OYmYm z86RJ9cZl5SuR6emPB>yrO)xg5>VucBcrV3UxTgZcUu(pYr+Sa=vl>4ql{NQy4-T%M zlCPf>t}rpgAS15uevdwJR_*5_H?USp=RR?a>$gSk-+w;VuIhukt9186ppP=Lzy1L7 ztx(smiwEKL>hkjH7Y))GcUk`Y z5ECCi%1tZE!rM4TU=lk^UdvMlTfvxem>?j&r?OZ>W4w?APw@uZ8qL`fTtS zQtB<7SczI&5ZKELNH8DU6UNe1SFyvU%S#WTlf%`QC8Z+*k{IQx`J}f79r+Sj-x|4f<|Jux>{!M|pWYf+ z-ST5a#Kn+V{DNZ0224A_ddrj3nA#XfsiTE9S+P9jnY<}MtGSKvVl|Em)=o#A607CfVjjA9S%vhb@C~*a2EQP= zy%omjzEs5x58jMrb>4HOurbxT7SUM@$dcH_k6U7LsyzmU9Bx3>q_Ct|QX{Zxr4Fz@ zGJYP!*yY~eryK`JRpCpC84p3mL?Gk0Gh48K+R$+<|KOB+nBL`QDC%?)zHXgyxS2}o zf!(A9x9Wgcv%(sn!?7Ec!-?CcP%no4K?dJHyyT)*$AiuGoyt=pM`gqw%S^@k8>V0V z4i~0?c>K{$I?NY;_`hy_j6Q{m~KDzkiGK z_ffu;+_e|m4d z_15oa@X;ab>43Run@zSszD-O!Nzy19m6h=R&twRlK+X3D)oKgLC~V9;J?XX>R3RGu zN|Unh(=HTQW)p?RT?&YNvMAv~vJ}zfcgn1a!J8UlwFd3TtiZ$TBtodeiE>6A>GCn^ zA`xU#q8UUJtPK*ND2fFUQVkl2(kzOkmFLZzH`JfJN%Y^99H_ZWjp0hL1Vw3?&qfM3@+M^tH0_;<4Av)eE{sGa?Elr02@SE;bbunGzjUVjG@9;L4^O6+mQdA@W)T zTdtB=Wto*&%qAF#4J`~oH7H5JSQw_Dpu=pkcTn!@9BcAv(bAckzhn|(lO7cYmsWwQ zN(_flmMl~iTNR2`&kt!SQrAcDY-u4!ZVrZ|Q3XOkVMWs#p&I(S^f6PD*XMguv@Vfl4&3R1!c}Ak zBM$#qTN-oX{d(%C%SrM_<*eHXl(?L3Mg==FJ2!2c)#Ow0-i>BCO;Qa9yt3Tb^7_mL zwro0iju!_*mSh=n>&{rY!B2_kLrPvTKiU~GH>}Il_aG5{LX1VsQEHw*hq0g$>i8&L z^J{Kn@_5#Z;7fs>a=lwox7_>f(`d(Km)uh&t$P_kzUi>LH;-Ah-hm)|V#J7yMiZ8) zoT4U>gw4kXOYWe-T=wlRp6!uz>t8CLLfy=`j@KUyZv;hyTVD%zSoUr+p4+Y&C$JR` zBLz45pFnB`Ag$l-2*UxHN0HvlE-;D%Uka-_M?VIu9|NBS)jpJz!QLzOJ+bi&NA0jE zEHVN-`b9V=TIL|?d!DtKK|r|TuiC*7p%J2n4xtB;kY3(M(4AQ14fR=3cDE={p=1hc z2hi^e3gAJ#f#Q4kS{M`;xDQ}UJ1milbEbWyNIF#gLCO@wCLYp)^#TiYQ14g^5X~rZ zg@vM!RKaL+1&I_Vbccb04rS@hET`LIZ+XDYe$`7j2Q7}R?A0aGJfiV{c<2p;kZB>-U zQEN|->iffGid`~+-bj7N#NHG+UOWHh#zy{=tLdR??HPLP-~=X04ZUtcjU&e8!wQP! z`+$WGQ^2o|tIno1Rg<;*UpI|iX|+k7x3zttENrC?7FcN|4HmOL6z7=(Cj}#rIu5Z+ zTEN;1Yo73xqzv{Yw}{r*T5EIM5!cA>E1c>XE08)aE2*k-yE-?Pje$)5dIR7P*TSUl z8C*d!g;gGY@TT4B(r}4enl8-t(arQsGX}dc4rV-HY-26MiHx|ry7w{R^>as@Gh#6O|j}_Gy*U>z}Is<#1ysnyhy3cq|$9zaz=lU1M zZJz1tbH;b4Z?~W8jAp^#kw2&kPe^ILsmc_m$P8nZIuZL|m!{R}FSRk)xG@{NFld&< zz$VX0NZXZNH%he}fVmUyQ$zY7CTSovrQ^9b;1-eG*kA9MmC=2!Ay7ye?QWMlC#Gb5 zTHwU<<%!(N9OYa!QxJxf$b4nMXc+v!9^W3wp?jI8ds(1-O0a+=mC5I4kF%XRP`YqL z?6yR@EcyC3+Q>nzy)iuCvhEYi|C_7w&zR?b0gH0f&ef6CQNDPYNEoBB9H>!J)HXu( zAtE=R%tZxC#8eOzs%nIpLgx+0a%2Q-t_`iM(7Wu`d|I+-J*9>`W3StC7wdele$4yj zU-x8yN`ybnWhedK>AJk#_S*G&9Nqo71zL}$ep-O`JL8Gz+-Tue7~wk)$5{||7?1nf zcY3(H1BTgH_j`!AIHpqy2Wggz^!9$!HZ29_SOnd|Hg`hGX_l+Pk(2v1Ly`n5dQT}4m zR;x~`dICosOtpbb%oCR;0nE#x=Hl&IP2eaeYH4hV^-PN=t@q-Df{p#18#yM&%^
)Mf(9O=$zt#xQMm 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml index bfaaef28..ce9bc066 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,14 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 itdelatrisu opsu 0.10.1 + ${project.version} ${maven.build.timestamp} yyyy-MM-dd HH:mm + itdelatrisu.opsu.Opsu src @@ -67,30 +69,9 @@ ${jar} java - -Djava.library.path=${project.build.directory}/natives -cp - itdelatrisu.opsu.Opsu - - - - - jarsplice - install - - exec - - - java - ${basedir}/target - - -jar - -Dinput=opsu-${project.version}.jar - -Dmain=itdelatrisu.opsu.Opsu - -Doutput=opsu-${project.version}-runnable.jar - - - ${basedir}/tools/JarSplicePlus.jar + ${mainClassName} @@ -98,7 +79,7 @@ maven-shade-plugin - 2.3 + 2.4.1 From 0b33fed2d489234efd3abbe98ce7fdf191847c1d Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 27 Aug 2015 20:48:08 -0500 Subject: [PATCH 21/40] Minor follow-up to #127. Signed-off-by: Jeffrey Han --- .../opsu/beatmap/BeatmapParser.java | 4 ++-- src/itdelatrisu/opsu/ui/Cursor.java | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index 103ac6e2..80c6c2f2 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -142,9 +142,9 @@ public boolean accept(File dir, String name) { // check if beatmap is cached String path = String.format("%s/%s", dir.getName(), file.getName()); - if (map != null && map.containsKey(path)) { + Long lastModified = map.get(path); + if (map != null && lastModified != null) { // check last modified times - long lastModified = map.get(path); if (lastModified == file.lastModified()) { // add to cached beatmap list Beatmap beatmap = new Beatmap(file); diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index 2dea62f5..69a13cd0 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -51,14 +51,16 @@ public class Cursor { /** Cursor rotation angle. */ private float cursorAngle = 0f; - /** The milliseconds when the cursor was last pressed, used for scale animation */ + /** 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 scale animation */ + + /** 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 */ + /** 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 */ + + /** 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. */ @@ -137,20 +139,18 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); // scale cursor - float cursorSizeAnimated = 1f; - + float cursorScaleAnimated = 1f; if (skin.isCursorExpanded()) { if (lastCursorPressState != mousePressed) { lastCursorPressState = mousePressed; lastCursorPressTime = System.currentTimeMillis(); } - cursorSizeAnimated = (mousePressed ? 1f : 1.25f) + - ((mousePressed ? CURSOR_SCALE_CHANGE : -CURSOR_SCALE_CHANGE) * AnimationEquation.IN_OUT_CUBIC.calc( - Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME)); + 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 = Options.getCursorScale() * cursorSizeAnimated; + float cursorScale = cursorScaleAnimated * Options.getCursorScale(); if (cursorScale != 1f) { cursor = cursor.getScaledCopy(cursorScale); cursorTrail = cursorTrail.getScaledCopy(cursorScale); From f773a8ecf8a2b02c444b9326972047124b07e892 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 28 Aug 2015 11:07:03 -0500 Subject: [PATCH 22/40] Remove jinput dependency since controllers aren't used. jinput natives are no longer loaded or packaged in the jar. Excluded the lwjgl dependency within the slick2d package since it depends on an older version (2.9.1) of lwjgl. Signed-off-by: Jeffrey Han --- build.gradle | 8 ++++++-- pom.xml | 12 ++++++++++++ src/itdelatrisu/opsu/Opsu.java | 9 +++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 544eee25..33328c42 100644 --- a/build.gradle +++ b/build.gradle @@ -30,8 +30,12 @@ repositories { } dependencies { - compile 'org.lwjgl.lwjgl:lwjgl:2.9.3' - compile 'org.slick2d:slick2d-core:1.0.1' + compile('org.lwjgl.lwjgl:lwjgl:2.9.3') { + exclude group: 'net.java.jinput', module: 'jinput' + } + compile('org.slick2d:slick2d-core:1.0.1') { + exclude group: 'org.lwjgl.lwjgl', module: 'lwjgl' + } compile 'org.jcraft:jorbis:0.0.17' compile 'net.lingala.zip4j:zip4j:1.3.2' compile 'com.googlecode.soundlibs:jlayer:1.0.1-1' diff --git a/pom.xml b/pom.xml index 72e1f62e..ee62a0e3 100644 --- a/pom.xml +++ b/pom.xml @@ -126,11 +126,23 @@ org.lwjgl.lwjgl lwjgl 2.9.3 + + + net.java.jinput + jinput + + org.slick2d slick2d-core 1.0.1 + + + org.lwjgl.lwjgl + lwjgl + + org.jcraft diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index fc67d143..ec76eccc 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -30,6 +30,7 @@ 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; @@ -133,8 +134,9 @@ public void uncaughtException(Thread t, Throwable e) { } File nativeDir; - if ((nativeDir = new File("./target/natives/")).isDirectory() || - (nativeDir = new File("./build/natives/")).isDirectory()) + if (!Utils.isJarRunning() && ( + (nativeDir = new File("./target/natives/")).isDirectory() || + (nativeDir = new File("./build/natives/")).isDirectory())) ; else { nativeDir = NativeLoader.NATIVE_DIR; @@ -184,6 +186,9 @@ public void run() { }.start(); } + // disable jinput + Input.disableControllers(); + // start the game try { // loop until force exit From c4f54ecc0594cb867d24dde4386d768133b3ff1d Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 28 Aug 2015 19:41:02 -0500 Subject: [PATCH 23/40] Hitting the "f" key during a replay changes the playback speed. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/states/Game.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 985406f4..c22bab62 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -928,6 +928,13 @@ else if (key == Options.getGameKeyRight()) } } 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; From 34c7942f4bd3e70a80890e4b3d1176939ddfbd8f Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 28 Aug 2015 21:12:47 -0500 Subject: [PATCH 24/40] Fixed "Hidden" mod timing issues. (fixes #121) This introduces more accurate timing formulas associated with the "Hidden" mod (previously, in #115, these values were hardcoded). The values seem somewhat close to the values in osu!, but were not extensively tested. Also set an upper bound on the fade in time for hit objects proportional to the approach time, or else the timing values in the "Hidden" mod would be too inconsistent. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/Circle.java | 18 ++--- src/itdelatrisu/opsu/objects/Slider.java | 18 ++--- src/itdelatrisu/opsu/objects/Spinner.java | 12 ++-- src/itdelatrisu/opsu/states/Game.java | 82 +++++++++++++++-------- 4 files changed, 80 insertions(+), 50 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 7818ba58..f67be5ec 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -36,9 +36,6 @@ * 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; @@ -94,15 +91,20 @@ public Circle(HitObject hitObject, Game game, GameData data, Color color, boolea @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); if (GameMod.HIDDEN.isActive()) { - float fadeOutScale = -(float)(timeDiff-game.getApproachTime())/game.getDecayTime(); - float fadeOutAlpha = Utils.clamp(1-fadeOutScale, 0, 1); - alpha = Math.min(alpha, fadeOutAlpha); + 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); + } } float oldAlpha = Colors.WHITE_FADE.a; diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 160f74aa..82f48355 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -57,9 +57,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; @@ -179,9 +176,11 @@ public Slider(HitObject hitObject, Game game, GameData data, Color color, boolea @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(); @@ -212,9 +211,12 @@ public void draw(Graphics g, int trackPosition) { } } if (GameMod.HIDDEN.isActive()) { - float fadeOutScale = -(float) (timeDiff - game.getApproachTime()) / game.getDecayTime(); - float fadeOutAlpha = Utils.clamp(1 - fadeOutScale, 0, 1); - alpha = Math.min(alpha, fadeOutAlpha); + 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) ; // don't draw current combo number if already clicked diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 53d67639..1ac9d7cd 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -51,9 +51,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; @@ -70,6 +67,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; @@ -125,6 +125,7 @@ public static void init(GameContainer container, float difficulty) { */ public Spinner(HitObject hitObject, Game game, GameData data) { this.hitObject = hitObject; + this.game = game; this.data = data; /* @@ -176,11 +177,12 @@ public Spinner(HitObject hitObject, Game game, GameData data) { 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()) { diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index c22bab62..ac13f881 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -18,6 +18,25 @@ package itdelatrisu.opsu.states; +import java.io.File; +import java.util.LinkedList; +import java.util.Stack; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.Display; +import org.newdawn.slick.Animation; +import org.newdawn.slick.Color; +import org.newdawn.slick.GameContainer; +import org.newdawn.slick.Graphics; +import org.newdawn.slick.Image; +import org.newdawn.slick.Input; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.state.BasicGameState; +import org.newdawn.slick.state.StateBasedGame; +import org.newdawn.slick.state.transition.EmptyTransition; +import org.newdawn.slick.state.transition.FadeInTransition; +import org.newdawn.slick.state.transition.FadeOutTransition; + import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameImage; @@ -52,25 +71,6 @@ import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimationEquation; -import java.io.File; -import java.util.LinkedList; -import java.util.Stack; - -import org.lwjgl.input.Keyboard; -import org.lwjgl.opengl.Display; -import org.newdawn.slick.Animation; -import org.newdawn.slick.Color; -import org.newdawn.slick.GameContainer; -import org.newdawn.slick.Graphics; -import org.newdawn.slick.Image; -import org.newdawn.slick.Input; -import org.newdawn.slick.SlickException; -import org.newdawn.slick.state.BasicGameState; -import org.newdawn.slick.state.StateBasedGame; -import org.newdawn.slick.state.transition.EmptyTransition; -import org.newdawn.slick.state.transition.FadeInTransition; -import org.newdawn.slick.state.transition.FadeOutTransition; - /** * "Game" state. */ @@ -119,9 +119,14 @@ public enum Restart { /** Hit object approach time, in milliseconds. */ private int approachTime; - /** Decay time for elements in "Hidden" mod, in milliseconds. */ - //TODO: figure out actual formula for decay time - private int decayTime = 800; + /** 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; @@ -1480,6 +1485,12 @@ private void setMapModifiers() { int diameter = (int) (104 - (circleSize * 8)); HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER); + // approachRate (hit object approach time) + if (approachRate < 5) + approachTime = (int) (1800 - (approachRate * 120)); + else + approachTime = (int) (1200 - ((approachRate - 5) * 150)); + // initialize objects Circle.init(container, circleSize); Slider.init(container, circleSize, beatmap); @@ -1487,12 +1498,6 @@ private void setMapModifiers() { Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ? Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor()); - // approachRate (hit object approach time) - if (approachRate < 5) - approachTime = (int) (1800 - (approachRate * 120)); - else - approachTime = (int) (1200 - ((approachRate - 5) * 150)); - // overallDifficulty (hit result time offsets) hitResultOffset = new int[GameData.HIT_MAX]; hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6)); @@ -1511,6 +1516,14 @@ private void setMapModifiers() { // 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); } /** @@ -1534,10 +1547,21 @@ private void setMapModifiers() { */ 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 getDecayTime() { return decayTime; } + 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). From 6d284cdd80a8bb1a4b57679e9c1a001527d9fc3f Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Fri, 28 Aug 2015 21:29:21 -0500 Subject: [PATCH 25/40] Organize imports. Some of these got re-ordered when I updated Eclipse -- resetting to the original sort order. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Opsu.java | 30 +++++++-------- src/itdelatrisu/opsu/Options.java | 14 +++---- src/itdelatrisu/opsu/Utils.java | 18 ++++----- src/itdelatrisu/opsu/replay/Replay.java | 4 +- src/itdelatrisu/opsu/states/Game.java | 50 ++++++++++++------------- src/itdelatrisu/opsu/ui/Cursor.java | 2 +- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index ec76eccc..05ad06d3 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -18,6 +18,21 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.audio.MusicController; +import itdelatrisu.opsu.db.DBController; +import itdelatrisu.opsu.downloads.DownloadList; +import itdelatrisu.opsu.downloads.Updater; +import itdelatrisu.opsu.states.ButtonMenu; +import itdelatrisu.opsu.states.DownloadsMenu; +import itdelatrisu.opsu.states.Game; +import itdelatrisu.opsu.states.GamePauseMenu; +import itdelatrisu.opsu.states.GameRanking; +import itdelatrisu.opsu.states.MainMenu; +import itdelatrisu.opsu.states.OptionsMenu; +import itdelatrisu.opsu.states.SongMenu; +import itdelatrisu.opsu.states.Splash; +import itdelatrisu.opsu.ui.UI; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -40,21 +55,6 @@ import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; -import itdelatrisu.opsu.audio.MusicController; -import itdelatrisu.opsu.db.DBController; -import itdelatrisu.opsu.downloads.DownloadList; -import itdelatrisu.opsu.downloads.Updater; -import itdelatrisu.opsu.states.ButtonMenu; -import itdelatrisu.opsu.states.DownloadsMenu; -import itdelatrisu.opsu.states.Game; -import itdelatrisu.opsu.states.GamePauseMenu; -import itdelatrisu.opsu.states.GameRanking; -import itdelatrisu.opsu.states.MainMenu; -import itdelatrisu.opsu.states.OptionsMenu; -import itdelatrisu.opsu.states.SongMenu; -import itdelatrisu.opsu.states.Splash; -import itdelatrisu.opsu.ui.UI; - /** * Main class. *

diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 25dddfa2..118a36f4 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -18,6 +18,13 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.audio.MusicController; +import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.skins.Skin; +import itdelatrisu.opsu.skins.SkinLoader; +import itdelatrisu.opsu.ui.Fonts; +import itdelatrisu.opsu.ui.UI; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -44,13 +51,6 @@ import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; -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; - /** * Handles all user options. */ diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index f1b1e613..f1f2fdfd 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -18,6 +18,15 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.audio.SoundController; +import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.downloads.Download; +import itdelatrisu.opsu.downloads.DownloadNode; +import itdelatrisu.opsu.replay.PlaybackSpeed; +import itdelatrisu.opsu.ui.Fonts; +import itdelatrisu.opsu.ui.UI; + import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.BufferedReader; @@ -58,15 +67,6 @@ import com.sun.jna.platform.FileUtils; -import itdelatrisu.opsu.audio.SoundController; -import itdelatrisu.opsu.audio.SoundEffect; -import itdelatrisu.opsu.beatmap.HitObject; -import itdelatrisu.opsu.downloads.Download; -import itdelatrisu.opsu.downloads.DownloadNode; -import itdelatrisu.opsu.replay.PlaybackSpeed; -import itdelatrisu.opsu.ui.Fonts; -import itdelatrisu.opsu.ui.UI; - /** * Contains miscellaneous utilities. */ diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java index 505ba128..6f2d9fb4 100644 --- a/src/itdelatrisu/opsu/replay/Replay.java +++ b/src/itdelatrisu/opsu/replay/Replay.java @@ -41,11 +41,11 @@ 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 diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index ac13f881..eb3b02d3 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -18,25 +18,6 @@ package itdelatrisu.opsu.states; -import java.io.File; -import java.util.LinkedList; -import java.util.Stack; - -import org.lwjgl.input.Keyboard; -import org.lwjgl.opengl.Display; -import org.newdawn.slick.Animation; -import org.newdawn.slick.Color; -import org.newdawn.slick.GameContainer; -import org.newdawn.slick.Graphics; -import org.newdawn.slick.Image; -import org.newdawn.slick.Input; -import org.newdawn.slick.SlickException; -import org.newdawn.slick.state.BasicGameState; -import org.newdawn.slick.state.StateBasedGame; -import org.newdawn.slick.state.transition.EmptyTransition; -import org.newdawn.slick.state.transition.FadeInTransition; -import org.newdawn.slick.state.transition.FadeOutTransition; - import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameData; import itdelatrisu.opsu.GameImage; @@ -71,6 +52,25 @@ import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.animations.AnimationEquation; +import java.io.File; +import java.util.LinkedList; +import java.util.Stack; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.Display; +import org.newdawn.slick.Animation; +import org.newdawn.slick.Color; +import org.newdawn.slick.GameContainer; +import org.newdawn.slick.Graphics; +import org.newdawn.slick.Image; +import org.newdawn.slick.Input; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.state.BasicGameState; +import org.newdawn.slick.state.StateBasedGame; +import org.newdawn.slick.state.transition.EmptyTransition; +import org.newdawn.slick.state.transition.FadeInTransition; +import org.newdawn.slick.state.transition.FadeOutTransition; + /** * "Game" state. */ @@ -1485,12 +1485,6 @@ private void setMapModifiers() { int diameter = (int) (104 - (circleSize * 8)); HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER); - // approachRate (hit object approach time) - if (approachRate < 5) - approachTime = (int) (1800 - (approachRate * 120)); - else - approachTime = (int) (1200 - ((approachRate - 5) * 150)); - // initialize objects Circle.init(container, circleSize); Slider.init(container, circleSize, beatmap); @@ -1498,6 +1492,12 @@ private void setMapModifiers() { Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ? Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor()); + // approachRate (hit object approach time) + if (approachRate < 5) + approachTime = (int) (1800 - (approachRate * 120)); + else + approachTime = (int) (1200 - ((approachRate - 5) * 150)); + // overallDifficulty (hit result time offsets) hitResultOffset = new int[GameData.HIT_MAX]; hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6)); diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index 69a13cd0..c7b46ffb 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -24,12 +24,12 @@ import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.skins.Skin; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.nio.IntBuffer; import java.util.Iterator; import java.util.LinkedList; -import itdelatrisu.opsu.ui.animations.AnimationEquation; import org.lwjgl.BufferUtils; import org.lwjgl.LWJGLException; import org.newdawn.slick.GameContainer; From 2d83c3a3485bb6dfdc936d9767076f0c95e40e4f Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 29 Aug 2015 00:32:08 -0500 Subject: [PATCH 26/40] Added "Directory Structure" section to the readme, with other changes. Signed-off-by: Jeffrey Han --- README.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4b4e667f..b23946fa 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # [opsu!](http://itdelatrisu.github.io/opsu/) -**opsu!** is an unofficial open-source client for [osu!](https://osu.ppy.sh/), -a rhythm game based on popular commercial games such as *Ouendan* and -*Elite Beat Agents*. It is written in Java using [Slick2D](http://slick.ninjacave.com/) -and [LWJGL](http://lwjgl.org/), wrappers around the OpenGL and OpenAL libraries. +**opsu!** is an unofficial open-source client for the rhythm game +[osu!](https://osu.ppy.sh/). It is written in Java using +[Slick2D](http://slick.ninjacave.com/) and [LWJGL](http://lwjgl.org/), +wrappers around the OpenGL and OpenAL libraries. -opsu! runs on Windows, OS X, and Linux platforms. A [libGDX port](https://github.com/fluddokt/opsu) -additionally supports Android devices. +opsu! runs on Windows, OS X, and Linux platforms. +A [libGDX port](https://github.com/fluddokt/opsu) additionally supports Android +devices. ## Getting Started Precompiled binaries for opsu! can be found on the [releases](https://github.com/itdelatrisu/opsu/releases) page, with the latest -builds at the top. APK releases can be found [here](https://github.com/fluddokt/opsu/releases). +builds at the top. APK releases can be found +[here](https://github.com/fluddokt/opsu/releases). ### Java Setup The Java Runtime Environment (JRE) must be installed in order to run opsu!. @@ -19,24 +21,46 @@ The download page is located [here](https://www.java.com/en/download/). ### Beatmaps opsu! requires beatmaps to run, which are available for download on the [osu! website](https://osu.ppy.sh/p/beatmaplist) and mirror sites such as -[osu!Mirror](https://osu.yas-online.net/) or [Bloodcat](http://bloodcat.com/osu/). +[osu!Mirror](https://osu.yas-online.net/) and [Bloodcat](http://bloodcat.com/osu/). Beatmaps can also be downloaded directly through opsu! in the downloads menu. -If osu! is already installed, this application will attempt to load songs -directly from the osu! program folder. Otherwise, place songs in the generated -`Songs` folder or set the `BeatmapDirectory` value in the generated -configuration file to the path of the root song directory. +If osu! is already installed, this application will attempt to load beatmaps +directly from the osu! program folder. Otherwise, place beatmaps in the +generated `Songs` folder or set the "BeatmapDirectory" value in the generated +configuration file to the path of the root beatmap directory. Note that beatmaps are typically delivered as OSZ files. These can be extracted with any ZIP tool, and opsu! will automatically extract them into the songs folder if placed in the `SongPacks` directory. ### First Run -The `Music Offset` value will likely need to be adjusted when playing for the +The "Music Offset" value will likely need to be adjusted when playing for the first time, or whenever hit objects are out of sync with the music. This and other game options can be accessed by clicking the "Other Options" button in the song menu. +### Directory Structure +The following files and folders will be created by opsu! as needed: +* `.opsu.cfg`: The configuration file. Most (but not all) of the settings can + be changed through the options menu. +* `.opsu.db`: The beatmap cache database. +* `.opsu_scores.db`: The scores database. +* `Songs/`: The beatmap directory (not used if an osu! installation is detected). + The parser searches all of its subdirectories for .osu files to load. +* `SongPacks/`: The beatmap pack directory. The unpacker extracts all .osz + files within this directory to the beatmap directory. +* `Skins/`: The skins directory. Each skin must be placed in a folder within + this directory. Any game resource (in `res/`) can be skinned by placing a + file with the same name in a skin folder. Skins can be selected in the + options menu. +* `Screenshots/`: The screenshot directory. Screenshots can be taken by + pressing the F12 key. +* `Replays/`: The replay directory. Replays of each completed game are saved + as .osr files, and can be viewed at a later time or shared with others. +* `ReplayImport/`: The replay import directory. The importer moves all .osr + files within this directory to the replay directory and saves the scores in + the scores database. Replays can be imported from osu! as well as opsu!. + ## Building opsu! is distributed as both a [Maven](https://maven.apache.org/) and [Gradle](https://gradle.org/) project. From c2839759b3e3bf300f7f3ff9ada017e8a4cb6462 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 29 Aug 2015 00:46:39 -0500 Subject: [PATCH 27/40] Follow-up to 2d83c3a: forgot to include a description of .opsu.log. Signed-off-by: Jeffrey Han --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b23946fa..37983e58 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ generated `Songs` folder or set the "BeatmapDirectory" value in the generated configuration file to the path of the root beatmap directory. Note that beatmaps are typically delivered as OSZ files. These can be extracted -with any ZIP tool, and opsu! will automatically extract them into the songs +with any ZIP tool, and opsu! will automatically extract them into the beatmap folder if placed in the `SongPacks` directory. ### First Run @@ -45,6 +45,8 @@ The following files and folders will be created by opsu! as needed: be changed through the options menu. * `.opsu.db`: The beatmap cache database. * `.opsu_scores.db`: The scores database. +* `.opsu.log`: The error log. All critical errors displayed in-game are also + logged to this file, and other warnings not shown are logged as well. * `Songs/`: The beatmap directory (not used if an osu! installation is detected). The parser searches all of its subdirectories for .osu files to load. * `SongPacks/`: The beatmap pack directory. The unpacker extracts all .osz From fad44356e621c3f5d5a3b9b8b1b60a22f4eec7c3 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 29 Aug 2015 13:05:24 +0200 Subject: [PATCH 28/40] Make the XDG-option available from command line I've found no way to access it without this workaround: https://discuss.gradle.org/t/can-i-override-the-group-property-from-the-command-line/5294 It's now possible to build with `gradle jar -PXDG=true`. --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 33328c42..1557c417 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,9 @@ mainClassName = 'itdelatrisu.opsu.Opsu' buildDir = new File(rootProject.projectDir, "build/") def useXDG = 'false' +if(hasProperty('XDG')) { + useXDG = XDG +} sourceCompatibility = 1.7 targetCompatibility = 1.7 @@ -103,4 +106,4 @@ jar { run { dependsOn 'unpackNatives' -} \ No newline at end of file +} From 9fc00fc3c383ec23842dc76869033da4af041fc2 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 29 Aug 2015 09:51:10 -0500 Subject: [PATCH 29/40] Follow-up to #128: add XDG info to readme, rename the Maven property. Signed-off-by: Jeffrey Han --- README.md | 9 +++++++-- build.gradle | 2 +- pom.xml | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 37983e58..cc1455a2 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The following files and folders will be created by opsu! as needed: * `ReplayImport/`: The replay import directory. The importer moves all .osr files within this directory to the replay directory and saves the scores in the scores database. Replays can be imported from osu! as well as opsu!. +* `Natives/`: The native libraries directory. ## Building opsu! is distributed as both a [Maven](https://maven.apache.org/) and @@ -72,14 +73,18 @@ Maven builds are built to the `target` directory. * To run the project, execute the Maven goal `compile`. * To create a single executable jar, execute the Maven goal `package -Djar`. This will compile a jar to `target/opsu-${version}.jar` with the libraries, - resources and natives packed inside the jar. + resources and natives packed inside the jar. Setting the "XDG" property + (`-DXDG=true`) will make the application use XDG folders under Unix-like + operating systems. ### Gradle Gradle builds are built to the `build` directory. * To run the project, execute the Gradle task `run`. * To create a single executable jar, execute the Gradle task `jar`. This will compile a jar to `build/libs/opsu-${version}.jar` with the libraries, - resources and natives packed inside the jar. + resources and natives packed inside the jar. Setting the "XDG" property + (`-PXDG=true`) will make the application use XDG folders under Unix-like + operating systems. ## Credits This software was created by Jeffrey Han diff --git a/build.gradle b/build.gradle index 1557c417..5bed43e0 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ mainClassName = 'itdelatrisu.opsu.Opsu' buildDir = new File(rootProject.projectDir, "build/") def useXDG = 'false' -if(hasProperty('XDG')) { +if (hasProperty('XDG')) { useXDG = XDG } diff --git a/pom.xml b/pom.xml index ee62a0e3..a51370d0 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ ${maven.build.timestamp} yyyy-MM-dd HH:mm itdelatrisu.opsu.Opsu - false + false src @@ -103,7 +103,7 @@ ${mainClassName} - ${useXDG} + ${XDG} From 31d0c237dffee5aa586fdda49a08e2e75d03a8d2 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 14:31:01 -0500 Subject: [PATCH 30/40] Create Natives dir in XDG_CACHE_HOME, not the working dir. (fixes #129) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/NativeLoader.java | 18 +++++++++++++----- src/itdelatrisu/opsu/Opsu.java | 4 ++-- src/itdelatrisu/opsu/Options.java | 6 ++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/itdelatrisu/opsu/NativeLoader.java b/src/itdelatrisu/opsu/NativeLoader.java index c66ee86c..a3ed28cd 100644 --- a/src/itdelatrisu/opsu/NativeLoader.java +++ b/src/itdelatrisu/opsu/NativeLoader.java @@ -33,16 +33,24 @@ * @author http://ninjacave.com */ public class NativeLoader { - /** Directory where natives are unpacked. */ - public static final File NATIVE_DIR = new File("Natives/"); + /** 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 */ public void loadNatives() throws IOException { - if (!NATIVE_DIR.exists()) - NATIVE_DIR.mkdir(); + if (!nativeDir.exists()) + nativeDir.mkdir(); JarFile jarFile = Utils.getJarFile(); if (jarFile == null) @@ -54,7 +62,7 @@ public void loadNatives() throws IOException { if (e == null) break; - File f = new File(NATIVE_DIR, e.getName()); + 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); diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 05ad06d3..7cabc6dd 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -139,9 +139,9 @@ public void uncaughtException(Thread t, Throwable e) { (nativeDir = new File("./build/natives/")).isDirectory())) ; else { - nativeDir = NativeLoader.NATIVE_DIR; + nativeDir = Options.NATIVE_DIR; try { - new NativeLoader().loadNatives(); + new NativeLoader(nativeDir).loadNatives(); } catch (IOException e) { Log.error("Error loading natives.", e); } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 118a36f4..d6f7623f 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -64,6 +64,9 @@ public class Options { /** 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"); @@ -90,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"; From 9d19dacab49957ba53eafe6ca1dca76b7cb4c361 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 17:56:05 -0500 Subject: [PATCH 31/40] Removed some unneeded methods. - Removed "isImplemented" field from GameMod as all of the base mods have been implemented. - Removed Utils.getBoundedValue() methods in preference for Utils.clamp(). Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameMod.java | 40 +--------------- src/itdelatrisu/opsu/NativeLoader.java | 6 +-- src/itdelatrisu/opsu/Options.java | 2 +- src/itdelatrisu/opsu/Utils.java | 48 +++---------------- src/itdelatrisu/opsu/states/ButtonMenu.java | 2 +- src/itdelatrisu/opsu/ui/MenuButton.java | 2 +- src/itdelatrisu/opsu/ui/UI.java | 2 +- .../opsu/ui/animations/AnimatedValue.java | 2 +- 8 files changed, 16 insertions(+), 88 deletions(-) diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index 4ef301ae..f59f9bc0 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -149,9 +149,6 @@ public void init(int width, int height) { /** The score multiplier. */ private final float multiplier; - /** Whether or not the mod is implemented. */ - private final boolean implemented; - /** The name of the mod. */ private final String name; @@ -313,24 +310,6 @@ public static String getModString(int state) { */ 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; @@ -338,7 +317,6 @@ public static String getModString(int state) { this.bit = bit; this.key = key; this.multiplier = multiplier; - this.implemented = implemented; this.name = name; this.description = description; } @@ -380,20 +358,11 @@ public static String getModString(int state) { */ 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; @@ -450,14 +419,7 @@ public void toggle(boolean checkInverse) { /** * 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. diff --git a/src/itdelatrisu/opsu/NativeLoader.java b/src/itdelatrisu/opsu/NativeLoader.java index a3ed28cd..6984baf0 100644 --- a/src/itdelatrisu/opsu/NativeLoader.java +++ b/src/itdelatrisu/opsu/NativeLoader.java @@ -95,10 +95,10 @@ private boolean isNativeFile(String entryName) { } else if (osName.startsWith("Linux")) { if (name.endsWith(".so")) return true; - } else if (((osName.startsWith("Mac")) || (osName.startsWith("Darwin"))) && ((name.endsWith(".jnilib")) || (name.endsWith(".dylib")))) { - return true; + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + if (name.endsWith(".dylib") || name.endsWith(".jnilib")) + return true; } - return false; } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index d6f7623f..40d5bc3e 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -640,7 +640,7 @@ else if (type == OptionType.BOOLEAN) */ 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); } /** diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index f1f2fdfd..e20ab7e9 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -156,40 +156,6 @@ public static void drawCentered(Animation anim, float x, float y) { anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f)); } - /** - * 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 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; - return val; - } - /** * Clamps a value between a lower and upper bound. * @param val the value to clamp @@ -236,6 +202,13 @@ public static float distance(float x1, float y1, float x2, float y2) { 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 @@ -603,11 +576,4 @@ public static File getRunningDirectory() { public static boolean parseBoolean(String s) { return (Integer.parseInt(s) == 1); } - - /** - * Linear interpolation of a and b at t. - */ - public static float lerp(float a, float b, float t) { - return a * (1 - t) + b * t; - } } diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index 1f21300e..23843f35 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -220,7 +220,7 @@ public void update(GameContainer container, int delta, int mouseX, int mouseY) { } // tooltips - if (hoverMod != null && hoverMod.isImplemented()) + if (hoverMod != null) UI.updateTooltip(delta, hoverMod.getDescription(), true); } diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index e9fa1089..99c454f2 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -460,7 +460,7 @@ public void autoHoverUpdate(int delta, boolean reverseAtEnd) { return; int d = delta * (autoAnimationForward ? 1 : -1); - if (Utils.getBoundedValue(time, d, 0, animationDuration) == time) { + if (Utils.clamp(time + d, 0, animationDuration) == time) { if (reverseAtEnd) autoAnimationForward = !autoAnimationForward; else { diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index b855aa8b..a067b1b1 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -263,7 +263,7 @@ private static void updateVolumeDisplay(int delta) { */ 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) diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java index 5e1636f9..d1a7fcdf 100644 --- a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java +++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java @@ -105,7 +105,7 @@ public void setEquation(AnimationEquation eqn) { * @return true if an update was applied, false if the animation was not updated */ public boolean update(int delta) { - int newTime = Utils.getBoundedValue(time, delta, 0, duration); + int newTime = Utils.clamp(time + delta, 0, duration); if (time != newTime) { this.time = newTime; updateValue(); From 8892973d98e04ebaa6656fe2a23749e61a122705 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 19:01:40 -0500 Subject: [PATCH 32/40] Now using official formulas for circle diameter and timing offsets. The previous formulas were extremely close, so you shouldn't notice any gameplay differences at all. Circle diameter: - Previously: 104 - (CS * 8) - Now: 108.848 - (CS * 8.9646) Timing offsets: added 1.5ms to 300, 100, and 50 hit result offsets. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/Circle.java | 7 +++---- src/itdelatrisu/opsu/objects/Slider.java | 7 +++---- src/itdelatrisu/opsu/objects/curves/Curve.java | 6 +++--- src/itdelatrisu/opsu/render/CurveRenderState.java | 7 +++---- src/itdelatrisu/opsu/states/Game.java | 15 ++++++++------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index f67be5ec..fca7073e 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -60,11 +60,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)); diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 82f48355..12bfa93a 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -111,15 +111,14 @@ 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; diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index ec8a199d..9caabe41 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -80,16 +80,16 @@ protected Curve(HitObject hitObject, Color color) { * 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; 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."); diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index dffd91f7..2d1ac1ad 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -64,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); } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index eb3b02d3..61a5e41e 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -1482,14 +1482,15 @@ private void setMapModifiers() { // 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) @@ -1500,9 +1501,9 @@ private void setMapModifiers() { // 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); From 8d21fe3fc900ac2f705aad70054036db84814a3f Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 20:25:35 -0500 Subject: [PATCH 33/40] Fixed a small issue in the MnetworkServer HTML parsing. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java index 625f7015..4341512a 100644 --- a/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java @@ -83,7 +83,7 @@ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) thr // BPM: {{bpm}} | Total Time: {{m}}:{{s}}
// Genre: {{genre}} | Updated: {{MMM}} {{d}}, {{yyyy}}
List nodeList = new ArrayList(); - final String START_TAG = "

", HREF_TAG = "", HREF_TAG = "", UPDATED = "Updated: "; int index = -1; int nextIndex = html.indexOf(START_TAG, index + 1); while ((index = nextIndex) != -1) { @@ -96,7 +96,7 @@ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) thr if (i == -1 || i > n) continue; i = html.indexOf('>', i + HREF_TAG.length()); if (i == -1 || i >= n) continue; - j = html.indexOf('<', i + 1); + j = html.indexOf(HREF_TAG_END, i + 1); if (j == -1 || j > n) continue; String beatmap = html.substring(i + 1, j).trim(); From bd8e35cb81e5fefaabf586d4193c386de9648e6e Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 22:20:52 -0500 Subject: [PATCH 34/40] Added MengSky download server. URL: http://osu.mengsky.net/ Also parsing raw HTML for this one because there's no API. Signed-off-by: Jeffrey Han --- .../opsu/downloads/servers/MengSkyServer.java | 204 ++++++++++++++++++ .../opsu/states/DownloadsMenu.java | 4 +- 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java diff --git a/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java new file mode 100644 index 00000000..829a9094 --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java @@ -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 . + */ + +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: + //
+ //
+ //
+ //
+ // Creator: {{creator}}
+ // MaxBpm: {{bpm}}
+ // Title: {{titleUnicode}}
+ // Artist: {{artistUnicode}}
+ // Status: {{"Ranked?" || "Unranked"}}
+ //
+ //

+ // Fork: bloodcat
+ // UpdateTime: {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}
+ // Mode: {{...}} + //
+ //
+ // Osu.ppy + //
+ //
+ // DownLoad + //
+ //
+ List nodeList = new ArrayList(); + final String + START_TAG = "
", + CREATOR_TAG = "Creator: ", TITLE_TAG = "Title: ", ARTIST_TAG = "Artist: ", + TIMESTAMP_TAG = "UpdateTime: ", DOWNLOAD_TAG = "
", + BR_TAG = "
", HREF_TAG = "
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; + } + + DownloadNode node = new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator); + System.out.println(node); + nodeList.add(node); + } + + 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; } +} diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 219f61a3..1c649548 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -35,6 +35,7 @@ import itdelatrisu.opsu.downloads.servers.BloodcatServer; import itdelatrisu.opsu.downloads.servers.DownloadServer; import itdelatrisu.opsu.downloads.servers.HexideServer; +import itdelatrisu.opsu.downloads.servers.MengSkyServer; import itdelatrisu.opsu.downloads.servers.MnetworkServer; import itdelatrisu.opsu.downloads.servers.YaSOnlineServer; import itdelatrisu.opsu.ui.Colors; @@ -78,7 +79,8 @@ public class DownloadsMenu extends BasicGameState { /** Available beatmap download servers. */ private static final DownloadServer[] SERVERS = { - new BloodcatServer(), new HexideServer(), new YaSOnlineServer(), new MnetworkServer() + new BloodcatServer(), new HexideServer(), new YaSOnlineServer(), + new MnetworkServer(), new MengSkyServer() }; /** The beatmap download server index. */ From aed5163a832c6bdd237f65009e560d5c3dbff16d Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 23:18:46 -0500 Subject: [PATCH 35/40] Check completed download size against Content-Length header. If the number of bytes received is less than the reported content length (e.g. a network timeout), mark the download with the "error" status instead of "complete". Content-Length should be reliable if reported at all, so this should be a valid approach. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/downloads/Download.java | 15 ++++++++++++--- .../opsu/downloads/servers/MengSkyServer.java | 4 +--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index a3f62390..09ebe292 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -245,9 +245,18 @@ else if (redirectCount > MAX_REDIRECTS) 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(); @@ -320,7 +329,7 @@ public float getProgress() { public long readSoFar() { switch (status) { case COMPLETE: - return contentLength; + return (rbc != null) ? rbc.getReadSoFar() : contentLength; case DOWNLOADING: if (rbc != null) return rbc.getReadSoFar(); diff --git a/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java index 829a9094..ce4647be 100644 --- a/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java @@ -174,9 +174,7 @@ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) thr continue; } - DownloadNode node = new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator); - System.out.println(node); - nodeList.add(node); + nodeList.add(new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator)); } nodes = nodeList.toArray(new DownloadNode[nodeList.size()]); From e535a8884081a780ff704aa5ae0d90f5392c3421 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sun, 30 Aug 2015 23:21:58 -0500 Subject: [PATCH 36/40] Stop firing BeatmapWatchService events for intended file modifications. Specifically, during OSZ unpacking and for file deletion through the song menu. Triggering the beatmap reload event in these scenarios would be redundant. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/OszUnpacker.java | 7 +++++++ .../opsu/beatmap/BeatmapSetList.java | 11 ++++++++++ .../opsu/beatmap/BeatmapWatchService.java | 20 +++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/itdelatrisu/opsu/OszUnpacker.java b/src/itdelatrisu/opsu/OszUnpacker.java index b8bf1460..e1eb36ba 100644 --- a/src/itdelatrisu/opsu/OszUnpacker.java +++ b/src/itdelatrisu/opsu/OszUnpacker.java @@ -18,6 +18,8 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.beatmap.BeatmapWatchService; + import java.io.File; import java.io.FilenameFilter; import java.util.ArrayList; @@ -62,6 +64,9 @@ public boolean accept(File dir, String name) { } // 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 +78,8 @@ public boolean accept(File dir, String name) { dirs.add(songDir); } } + if (ws != null) + ws.resume(); fileIndex = -1; files = null; diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java index c146ee67..c37b4dd1 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSetList.java @@ -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; @@ -199,11 +200,16 @@ else if (ePrev != null && ePrev.index == expandedIndex) 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; } @@ -247,11 +253,16 @@ public boolean deleteSong(BeatmapSetNode node) { 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; } diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java index a287e198..e63482cf 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java @@ -162,6 +162,9 @@ public interface BeatmapWatchServiceListener { /** 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 @@ -242,6 +245,7 @@ private void processEvents() { if (dir == null) continue; + boolean isPaused = paused; for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) @@ -254,8 +258,10 @@ private void processEvents() { //System.out.printf("%s: %s\n", kind.name(), child); // fire listeners - for (BeatmapWatchServiceListener listener : listeners) - listener.eventReceived(kind, child); + 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) { @@ -272,4 +278,14 @@ private void processEvents() { } } } + + /** + * Stops listener events from being fired. + */ + public void pause() { paused = true; } + + /** + * Resumes firing listener events. + */ + public void resume() { paused = false; } } From c70fcb296f4524211df0bba58d60f7960be09c4d Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 31 Aug 2015 17:54:32 -0500 Subject: [PATCH 37/40] Add git hash (from refs/remotes/origin/master) to error reports. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ErrorHandler.java | 6 ++++++ src/itdelatrisu/opsu/Utils.java | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java index bad2c513..1dc4d227 100644 --- a/src/itdelatrisu/opsu/ErrorHandler.java +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -171,6 +171,12 @@ private static URI getIssueURI(String error, Throwable e, String trace) { 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"); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index e20ab7e9..87c0749b 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -32,6 +32,7 @@ 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; @@ -576,4 +577,29 @@ public static File getRunningDirectory() { 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; + } + } } From 769ad963e7aa636875c9ad673cb14c222a6ffb54 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 31 Aug 2015 22:25:28 -0500 Subject: [PATCH 38/40] Moved OszUnpacker into package 'opsu.beatmap'. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/{ => beatmap}/OszUnpacker.java | 5 +++-- src/itdelatrisu/opsu/states/DownloadsMenu.java | 2 +- src/itdelatrisu/opsu/states/SongMenu.java | 2 +- src/itdelatrisu/opsu/states/Splash.java | 2 +- src/itdelatrisu/opsu/ui/UI.java | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) rename src/itdelatrisu/opsu/{ => beatmap}/OszUnpacker.java (97%) diff --git a/src/itdelatrisu/opsu/OszUnpacker.java b/src/itdelatrisu/opsu/beatmap/OszUnpacker.java similarity index 97% rename from src/itdelatrisu/opsu/OszUnpacker.java rename to src/itdelatrisu/opsu/beatmap/OszUnpacker.java index e1eb36ba..5b9c1071 100644 --- a/src/itdelatrisu/opsu/OszUnpacker.java +++ b/src/itdelatrisu/opsu/beatmap/OszUnpacker.java @@ -16,9 +16,10 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu; +package itdelatrisu.opsu.beatmap; -import itdelatrisu.opsu.beatmap.BeatmapWatchService; +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Options; import java.io.File; import java.io.FilenameFilter; diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 1c649548..c4559263 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -21,7 +21,6 @@ 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,6 +28,7 @@ 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; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index be7ea09b..fe6f167c 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -24,7 +24,6 @@ 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; @@ -37,6 +36,7 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.beatmap.BeatmapSortOrder; import itdelatrisu.opsu.beatmap.BeatmapWatchService; +import itdelatrisu.opsu.beatmap.OszUnpacker; import itdelatrisu.opsu.beatmap.BeatmapWatchService.BeatmapWatchServiceListener; import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.ScoreDB; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index b5bed1ee..a0ea9963 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -21,13 +21,13 @@ 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; diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index a067b1b1..ec9195b3 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -21,10 +21,10 @@ 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; From df3e662865532b6607713cfc83cc9a2a428e6da9 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Mon, 31 Aug 2015 22:54:35 -0500 Subject: [PATCH 39/40] Marked a couple more members as 'final'. (continuation of 441bb95) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/downloads/DownloadNode.java | 10 +++++----- .../opsu/downloads/ReadableByteChannelWrapper.java | 2 +- src/itdelatrisu/opsu/objects/curves/CatmullCurve.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index cf9775c7..4205f058 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -44,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; diff --git a/src/itdelatrisu/opsu/downloads/ReadableByteChannelWrapper.java b/src/itdelatrisu/opsu/downloads/ReadableByteChannelWrapper.java index 786b2c5d..cb1a86d1 100644 --- a/src/itdelatrisu/opsu/downloads/ReadableByteChannelWrapper.java +++ b/src/itdelatrisu/opsu/downloads/ReadableByteChannelWrapper.java @@ -28,7 +28,7 @@ */ public class ReadableByteChannelWrapper implements ReadableByteChannel { /** The wrapped ReadableByteChannel. */ - private ReadableByteChannel rbc; + private final ReadableByteChannel rbc; /** The number of bytes read. */ private long bytesRead; diff --git a/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java index 95842e77..df59b032 100644 --- a/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java +++ b/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java @@ -61,9 +61,9 @@ public CatmullCurve(HitObject hitObject, Color color) { 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 (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]))); From e3d8a11c39a91d926f9a4e13b1f5c150555d5480 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 1 Sep 2015 16:12:41 -0500 Subject: [PATCH 40/40] Updating to 0.11.0. Signed-off-by: Jeffrey Han --- build.gradle | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5bed43e0..6c3e15e0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'application' import org.apache.tools.ant.filters.* group = 'itdelatrisu' -version = '0.10.1' +version = '0.11.0' mainClassName = 'itdelatrisu.opsu.Opsu' buildDir = new File(rootProject.projectDir, "build/") diff --git a/pom.xml b/pom.xml index a51370d0..f440c568 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 itdelatrisu opsu - 0.10.1 + 0.11.0 ${project.version} ${maven.build.timestamp}