diff --git a/res/slidergradient_ex.png b/res/slidergradient_ex.png
new file mode 100644
index 00000000..363fa418
Binary files /dev/null and b/res/slidergradient_ex.png differ
diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java
index 617386f7..a95d34a8 100644
--- a/src/itdelatrisu/opsu/Container.java
+++ b/src/itdelatrisu/opsu/Container.java
@@ -29,6 +29,7 @@
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.options.Options;
import itdelatrisu.opsu.render.CurveRenderState;
+import itdelatrisu.opsu.render.LegacyCurveRenderState;
import itdelatrisu.opsu.ui.UI;
import org.lwjgl.opengl.Display;
@@ -179,6 +180,7 @@ private void close_sub() {
// delete OpenGL objects involved in the Curve rendering
CurveRenderState.shutdown();
+ LegacyCurveRenderState.shutdown();
// destroy watch service
if (!Options.isWatchServiceEnabled())
diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java
index 842cf42e..619406eb 100644
--- a/src/itdelatrisu/opsu/GameData.java
+++ b/src/itdelatrisu/opsu/GameData.java
@@ -1088,14 +1088,18 @@ else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.re
*/
private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) {
// fade out slider curve
- if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null) {
+ if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null && !(Options.isExperimentalSliderStyle() && Options.isShrinkingSliders())) {
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float alpha = 1f - progress;
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Colors.WHITE_FADE.a = hitResult.color.a = alpha;
- hitResult.curve.draw(hitResult.color);
+ if (Options.isExperimentalSliderStyle()) {
+ hitResult.curve.draw(hitResult.color, Options.isMergingSliders() ? 1 : 0, hitResult.curve.getCurvePoints().length);
+ } else {
+ hitResult.curve.draw(hitResult.color);
+ }
Colors.WHITE_FADE.a = oldWhiteAlpha;
hitResult.color.a = oldColorAlpha;
}
@@ -1125,6 +1129,10 @@ private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) {
fc.drawCentered(hitResult.x, hitResult.y);
}
+ if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null && !Options.isDrawSliderEndCircles()) {
+ return;
+ }
+
// hit circles
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java
index deb5e402..49ed118b 100644
--- a/src/itdelatrisu/opsu/GameImage.java
+++ b/src/itdelatrisu/opsu/GameImage.java
@@ -105,6 +105,7 @@ protected Image process_sub(Image img, int w, int h) {
// Slider
SLIDER_GRADIENT ("slidergradient", "png"),
+ SLIDER_GRADIENT_EX ("slidergradient_ex", "png"),
SLIDER_BALL ("sliderb", "sliderb%d", "png"),
SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"),
REVERSEARROW ("reversearrow", "png"),
diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java
index 209d82a7..f4b816e2 100644
--- a/src/itdelatrisu/opsu/Utils.java
+++ b/src/itdelatrisu/opsu/Utils.java
@@ -235,6 +235,21 @@ public static float clamp(float val, float low, float high) {
return val;
}
+ /**
+ * Clamps a value between a lower and upper bound.
+ * @param val the value to clamp
+ * @param low the lower bound
+ * @param high the upper bound
+ * @return the clamped value
+ */
+ public static double clamp(double val, double low, double high) {
+ if (val < low)
+ return low;
+ if (val > high)
+ return high;
+ return val;
+ }
+
/**
* Returns the distance between two points.
* @param x1 the x-component of the first point
diff --git a/src/itdelatrisu/opsu/beatmap/HitObject.java b/src/itdelatrisu/opsu/beatmap/HitObject.java
index ca5f5790..07be1b5a 100644
--- a/src/itdelatrisu/opsu/beatmap/HitObject.java
+++ b/src/itdelatrisu/opsu/beatmap/HitObject.java
@@ -271,6 +271,19 @@ else if ((type & HitObject.TYPE_SLIDER) > 0) {
}
}
+ /**
+ * Constructor to make fake/temp hitobj
+ * @param x xpos
+ * @param y ypos
+ * @param time time
+ */
+ public HitObject( float x, float y, int time) {
+ this.x = x;
+ this.y = y;
+ this.time = time;
+ this.type = HitObject.TYPE_CIRCLE;
+ }
+
/**
* Returns the raw starting x coordinate.
*/
diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java
index 05e29237..14946983 100644
--- a/src/itdelatrisu/opsu/objects/Circle.java
+++ b/src/itdelatrisu/opsu/objects/Circle.java
@@ -38,7 +38,7 @@
*/
public class Circle implements GameObject {
/** The diameter of hit circles. */
- private static float diameter;
+ public static float diameter;
/** The associated HitObject. */
private HitObject hitObject;
diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java
index 96a75acd..25ed5f41 100644
--- a/src/itdelatrisu/opsu/objects/Slider.java
+++ b/src/itdelatrisu/opsu/objects/Slider.java
@@ -117,6 +117,12 @@ public class Slider implements GameObject {
/** Container dimensions. */
private static int containerWidth, containerHeight;
+ /** Curve color for experimental style sliders. */
+ private static Color curveColor = new Color(0, 0, 0, 20);
+
+ /** Start index of this slider in the merged slider. */
+ public int baseSliderFrom;
+
/**
* Initializes the Slider data type with images and dimensions.
* @param container the game container
@@ -212,11 +218,18 @@ public void draw(Graphics g, int trackPosition) {
Math.max(0f, 1f - ((float) (trackPosition - hitObject.getTime()) / (getEndTime() - hitObject.getTime())) * 1.05f);
}
- float curveInterval = Options.isSliderSnaking() ? alpha : 1f;
- curve.draw(color,curveInterval);
+ boolean isCurveCompletelyDrawn;
+ if (!Options.isExperimentalSliderStyle()) {
+ isCurveCompletelyDrawn = Options.isSliderSnaking() || alpha == 1f;
+ curve.draw(color, isCurveCompletelyDrawn ? 1f : alpha);
+ } else {
+ curveColor.a = sliderAlpha;
+ isCurveCompletelyDrawn = drawSliderTrack(trackPosition, Utils.clamp(1d - (double) (timeDiff - approachTime + fadeInTime) / fadeInTime, 0d, 1d));
+ color.a = alpha;
+ }
// end circle (only draw if ball still has to go there)
- if (curveInterval == 1f && currentRepeats < repeatCount - (repeatCount % 2 == 0 ? 1 : 0)) {
+ if (isCurveCompletelyDrawn && (!Options.isExperimentalSliderStyle() || Options.isDrawSliderEndCircles()) && currentRepeats < repeatCount - (repeatCount % 2 == 0 ? 1 : 0)) {
Color circleColor = new Color(color);
Color overlayColor = new Color(Colors.WHITE_FADE);
if (currentRepeats == 0) {
@@ -228,7 +241,7 @@ public void draw(Graphics g, int trackPosition) {
// fade in end circle after repeats
circleColor.a = overlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, true);
}
- Vec2f endCircPos = curve.pointAt(curveInterval);
+ Vec2f endCircPos = curve.pointAt(1f);
hitCircle.drawCentered(endCircPos.x, endCircPos.y, circleColor);
hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, overlayColor);
}
@@ -277,13 +290,15 @@ public void draw(Graphics g, int trackPosition) {
}
// repeats
- if (curveInterval == 1.0f) {
+ if (isCurveCompletelyDrawn) {
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1 && tcurRepeat < repeatCount - 1; tcurRepeat++) {
Image arrow = GameImage.REVERSEARROW.getImage();
// bouncing animation
//arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292));
- float colorLuminance = Utils.getLuminance(color);
- Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black;
+ Color arrowColor = Color.white;
+ if (!Options.isExperimentalSliderStyle() && Utils.getLuminance(color) >= 0.8f) {
+ arrowColor = Color.black;
+ }
if (tcurRepeat == 0) {
arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f);
} else {
@@ -406,6 +421,42 @@ private void drawSliderTicks(int trackPosition, float curveAlpha, float decorati
}
}
+ /**
+ * Draws the slider track for the experimental style sliders.
+ * @param trackPosition position of the song
+ * @param snakingSliderProgress progress of the snaking sliders [0, 1]
+ * @return true if the track was completely drawn
+ */
+ private boolean drawSliderTrack(int trackPosition, double snakingSliderProgress) {
+ double curveIntervalTo = Options.isSliderSnaking() ? snakingSliderProgress : 1d;
+ double curveIntervalFrom = 0d;
+ if (Options.isShrinkingSliders()) {
+ double sliderprogress = (trackPosition - hitObject.getTime() - ((double) sliderTime * (hitObject.getRepeatCount() - 1))) / (double) sliderTime;
+ if (sliderprogress > 0) {
+ curveIntervalFrom = sliderprogress;
+ }
+ }
+ int curvelen = curve.getCurvePoints().length;
+ if (Options.isMergingSliders()) {
+ if (Options.isShrinkingSliders() && curveIntervalFrom > 0) {
+ if (hitObject.getRepeatCount() % 2 == 0) {
+ game.addMergedSliderPointsToRender(baseSliderFrom, baseSliderFrom + (int) ((1d - curveIntervalFrom) * curvelen));
+ } else {
+ game.addMergedSliderPointsToRender(baseSliderFrom + (int) (curveIntervalFrom * curvelen) + 1, baseSliderFrom + (int) (curveIntervalTo * curve.getCurvePoints().length));
+ }
+ } else {
+ game.addMergedSliderPointsToRender(baseSliderFrom, baseSliderFrom + (int) (curveIntervalTo * curve.getCurvePoints().length));
+ }
+ } else {
+ if (Options.isShrinkingSliders() && curveIntervalFrom > 0 && hitObject.getRepeatCount() % 2 == 0) {
+ curve.splice((int) ((1d - curveIntervalFrom) * curvelen), curvelen);
+ curveIntervalFrom = 0d;
+ }
+ curve.draw(curveColor, (int) (curveIntervalFrom * curvelen), (int) (curveIntervalTo * curvelen));
+ }
+ return curveIntervalTo == 1d;
+ }
+
/**
* Get the alpha level used to fade in circles & reversearrows after repeat
* @param trackPosition current trackposition, in ms
@@ -707,7 +758,6 @@ else if (GameMod.RELAX.isActive() && trackPosition >= time)
// calculate and send slider result
hitResult();
-
return true;
}
@@ -753,6 +803,10 @@ private float getT(int trackPosition, boolean raw) {
}
}
+ public Curve getCurve() {
+ return curve;
+ }
+
@Override
public void reset() {
sliderClickedInitial = false;
diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java
index e4fd34c4..0afb7bd6 100644
--- a/src/itdelatrisu/opsu/objects/curves/Curve.java
+++ b/src/itdelatrisu/opsu/objects/curves/Curve.java
@@ -23,6 +23,7 @@
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.options.Options;
import itdelatrisu.opsu.render.CurveRenderState;
+import itdelatrisu.opsu.render.LegacyCurveRenderState;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.ui.Colors;
@@ -42,7 +43,7 @@ public abstract class Curve {
protected static float CURVE_POINTS_SEPERATION = 5;
/** The curve border color. */
- private static Color borderColor;
+ protected static Color borderColor;
/** Whether mmsliders are supported. */
private static boolean mmsliderSupported = false;
@@ -59,9 +60,20 @@ public abstract class Curve {
/** Per-curve render-state used for the new style curve renders. */
private CurveRenderState renderState;
+ /** Per-curve render-state used for the legacy style curve renders. */
+ protected LegacyCurveRenderState legacyRenderState;
+
/** Points along the curve (set by inherited classes). */
protected Vec2f[] curve;
+ /**
+ * Get the curve points
+ * @return curve points
+ */
+ public Vec2f[] getCurvePoints() {
+ return curve;
+ }
+
/**
* Constructor.
* @param hitObject the associated HitObject
@@ -96,9 +108,10 @@ public static void init(int width, int height, float circleDiameter, Color borde
ContextCapabilities capabilities = GLContext.getCapabilities();
mmsliderSupported = capabilities.OpenGL30;
- if (mmsliderSupported)
+ if (mmsliderSupported) {
CurveRenderState.init(width, height, circleDiameter);
- else {
+ LegacyCurveRenderState.init(width, height, circleDiameter);
+ } else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
Log.warn("New slider style requires OpenGL 3.0.");
}
@@ -147,6 +160,25 @@ public void draw(Color color, float t) {
}
}
+ public void draw(Color color, int from, int to) {
+ if (curve == null)
+ return;
+ if (legacyRenderState == null)
+ legacyRenderState = new LegacyCurveRenderState(hitObject, curve);
+ legacyRenderState.draw(color, borderColor, from, to);
+ }
+
+ /**
+ * Splice the curve to draw (= flag a part of it as 'do not draw'). Bases on the curve point indices.
+ * @param from start index to splice
+ * @param to end index to splice
+ */
+ public void splice(int from, int to) {
+ if (legacyRenderState == null)
+ return;
+ legacyRenderState.splice(from, to);
+ }
+
/**
* Returns the angle of the first control point.
*/
@@ -175,5 +207,7 @@ public void draw(Color color, float t) {
public void discardGeometry() {
if (renderState != null)
renderState.discardGeometry();
+ if (legacyRenderState != null)
+ legacyRenderState.discardGeometry();
}
}
diff --git a/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java b/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java
new file mode 100644
index 00000000..9050de23
--- /dev/null
+++ b/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java
@@ -0,0 +1,68 @@
+/*
+ * opsu! - an open-source osu! client
+ * Copyright (C) 2014-2017 Jeffrey Han
+ *
+ * opsu! is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * opsu! is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with opsu!. If not, see .
+ */
+package itdelatrisu.opsu.objects.curves;
+
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.render.LegacyCurveRenderState;
+import org.newdawn.slick.Color;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FakeCombinedCurve extends Curve {
+
+ private List pointsToRender;
+
+ public FakeCombinedCurve(Vec2f[] points) {
+ super(new HitObject(0, 0, 0), false);
+ this.curve = points;
+ pointsToRender = new ArrayList<>();
+ }
+
+ public void initForFrame() {
+ pointsToRender.clear();
+ }
+
+ public void addRange(int from, int to) {
+ pointsToRender.add(from);
+ pointsToRender.add(to);
+ }
+
+ @Override
+ public void draw(Color color) {
+ if (legacyRenderState == null)
+ legacyRenderState = new LegacyCurveRenderState(hitObject, curve);
+ legacyRenderState.draw(color, borderColor, pointsToRender);
+ }
+
+ @Override
+ public Vec2f pointAt(float t) {
+ return null;
+ }
+
+ @Override
+ public float getEndAngle() {
+ return 0;
+ }
+
+ @Override
+ public float getStartAngle() {
+ return 0;
+ }
+
+}
diff --git a/src/itdelatrisu/opsu/options/OptionGroup.java b/src/itdelatrisu/opsu/options/OptionGroup.java
index 07a3e244..cc5f8a95 100644
--- a/src/itdelatrisu/opsu/options/OptionGroup.java
+++ b/src/itdelatrisu/opsu/options/OptionGroup.java
@@ -51,6 +51,12 @@ public class OptionGroup {
GameOption.SHOW_FOLLOW_POINTS,
GameOption.SCREENSHOT_FORMAT,
}),
+ new OptionGroup("EXPERIMENTAL SLIDERS", new GameOption[] {
+ GameOption.EXPERIMENTAL_SLIDERS,
+ GameOption.EXPERIMENTAL_SLIDERS_MERGE,
+ GameOption.EXPERIMENTAL_SLIDERS_SHRINK,
+ GameOption.EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES,
+ }),
new OptionGroup("MAIN MENU", new GameOption[] {
GameOption.DYNAMIC_BACKGROUND,
GameOption.PARALLAX,
diff --git a/src/itdelatrisu/opsu/options/Options.java b/src/itdelatrisu/opsu/options/Options.java
index f907b5f6..8a1e913f 100644
--- a/src/itdelatrisu/opsu/options/Options.java
+++ b/src/itdelatrisu/opsu/options/Options.java
@@ -567,6 +567,10 @@ public void setValue(int value) {
IGNORE_BEATMAP_SKINS ("Ignore all beatmap skins", "IgnoreBeatmapSkins", "Defaults game settings to never use skin element overrides provided by beatmaps.", false),
FORCE_SKIN_CURSOR ("Always use skin cursor", "UseSkinCursor", "The selected skin's cursor will override any beatmap-specific cursor modifications.", false),
SNAKING_SLIDERS ("Snaking sliders", "SnakingSliders", "Sliders gradually snake out from their starting point.", true),
+ EXPERIMENTAL_SLIDERS ("Enable", "ExperimentalSliders", "Enable experimental slider style", false),
+ EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES ("Draw slider endcircles", "ExSlDrawEnd", "Only for experimental sliders", false),
+ EXPERIMENTAL_SLIDERS_SHRINK ("Shrinking sliders", "ExSlShrink", "Sliders shrink toward their ending point when the ball passes.", true),
+ EXPERIMENTAL_SLIDERS_MERGE ("Merging sliders", "ExSlMerge", "Don't draw edges and combine slider tracks where sliders overlap.", true),
SHOW_HIT_LIGHTING ("Hit lighting", "HitLighting", "Adds a subtle glow behind hit explosions which lights the playfield.", true),
SHOW_COMBO_BURSTS ("Combo bursts", "ComboBurst", "A character image bursts from the side of the screen at combo milestones.", true),
SHOW_PERFECT_HIT ("Perfect hits", "PerfectHit", "Shows perfect hit result bursts (300s, slider ticks).", true),
@@ -1176,6 +1180,11 @@ public static void setDisplayMode(Container app) {
*/
public static boolean isSliderSnaking() { return GameOption.SNAKING_SLIDERS.getBooleanValue(); }
+ public static boolean isExperimentalSliderStyle() { return GameOption.EXPERIMENTAL_SLIDERS.getBooleanValue(); }
+ public static boolean isDrawSliderEndCircles() { return GameOption.EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES.getBooleanValue(); }
+ public static boolean isShrinkingSliders() { return GameOption.EXPERIMENTAL_SLIDERS_SHRINK.getBooleanValue(); }
+ public static boolean isMergingSliders() { return GameOption.EXPERIMENTAL_SLIDERS_MERGE.getBooleanValue(); }
+
/**
* Returns the fixed circle size override, if any.
* @return the CS value (0, 10], 0f if disabled
diff --git a/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java b/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java
new file mode 100644
index 00000000..45bd0232
--- /dev/null
+++ b/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java
@@ -0,0 +1,595 @@
+/*
+ * opsu! - an open-source osu! client
+ * Copyright (C) 2014, 2015 Jeffrey Han
+ *
+ * opsu! is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * opsu! is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with opsu!. If not, see .
+ */
+package itdelatrisu.opsu.render;
+
+import itdelatrisu.opsu.GameImage;
+import itdelatrisu.opsu.Utils;
+import itdelatrisu.opsu.beatmap.HitObject;
+import itdelatrisu.opsu.objects.Circle;
+import itdelatrisu.opsu.objects.curves.Vec2f;
+import org.lwjgl.BufferUtils;
+import org.lwjgl.opengl.*;
+import org.newdawn.slick.Color;
+import org.newdawn.slick.Image;
+import org.newdawn.slick.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Hold the temporary render state that needs to be restored again after the new
+ * style curves are drawn.
+ *
+ * @author Bigpet {@literal }
+ */
+public class LegacyCurveRenderState {
+ /** The width and height of the display container this curve gets drawn into. */
+ protected static int containerWidth, containerHeight;
+
+ /** Thickness of the curve. */
+ protected static int scale;
+
+ /** Static state that's needed to draw the new style curves. */
+ private static final NewCurveStyleState staticState = new NewCurveStyleState();
+
+ /** Cached drawn slider, only used if new style sliders are activated. */
+ public Rendertarget fbo;
+
+ /** The HitObject associated with the curve to be drawn. */
+ protected HitObject hitObject;
+
+ protected Vec2f[] curve;
+
+ /** The point to which the curve has last been rendered into the texture (as an index into {@code curve}). */
+ private int lastPointDrawn;
+
+ /** The point from which the curve has last been rendered into the texture (as an index into {@code curve}). */
+ private int firstPointDrawn;
+
+ /** Index of the curve array that indicates the starting position of the slider part that should not be drawn. */
+ private int spliceFrom;
+
+ /** Index of the curve array that indicates the end position of the slider part that should not be drawn. */
+ private int spliceTo;
+
+ /** List holding pairs of 2 numbers in a sequential order, indicating the start- and endindex of the curvearray to render. */
+ protected List pointsToRender;
+
+ /**
+ * Set the width and height of the container that Curves get drawn into.
+ * Should be called before any curves are drawn.
+ * @param width the container width
+ * @param height the container height
+ * @param circleDiameter the circle diameter
+ */
+ public static void init(int width, int height, float circleDiameter) {
+ containerWidth = width;
+ containerHeight = height;
+
+ // equivalent to what happens in Slider.init()
+ 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);
+ NewCurveStyleState.initUnitCone();
+ }
+
+ /**
+ * Undo the static state. Static state setup caused by calls to
+ * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, int, int)}
+ * are undone.
+ */
+ public static void shutdown() {
+ staticState.shutdown();
+ FrameBufferCache.shutdown();
+ }
+
+ /**
+ * Creates an object to hold the render state that's necessary to draw a curve.
+ * @param hitObject the HitObject that represents this curve, just used as a unique ID
+ * @param curve the points along the curve to be drawn
+ */
+ public LegacyCurveRenderState(HitObject hitObject, Vec2f[] curve) {
+ this.hitObject = hitObject;
+ this.curve = curve;
+ initFBO();
+ }
+
+
+ private void initFBO() {
+ FrameBufferCache cache = FrameBufferCache.getInstance();
+ Rendertarget mapping = cache.get(hitObject);
+ if(mapping == null)
+ mapping = cache.insert(hitObject);
+ fbo = mapping;
+
+ createVertexBuffer(fbo.getVbo()
+
+ );
+ //write impossible value to make sure the fbo is cleared
+ lastPointDrawn=-1;
+ spliceFrom=spliceTo=-1;
+ }
+
+ /**
+ * Splice the curve
+ * @param from start index to splice
+ * @param to end index to splice
+ */
+ public void splice(int from, int to) {
+ spliceFrom = from * 2;
+ spliceTo = to * 2;
+ firstPointDrawn = -1; // force redraw
+ lastPointDrawn = -1; // force redraw
+ }
+
+ public void draw(Color color, Color borderColor, List pointsToRender) {
+ lastPointDrawn = -1;
+ firstPointDrawn = -1;
+ this.pointsToRender = pointsToRender;
+ draw(color, borderColor, 0, curve.length);
+ this.pointsToRender = null;
+ }
+
+ /**
+ * Draw a curve to the screen that's tinted with `color`. The first time
+ * this is called this caches the image result of the curve and on subsequent
+ * runs it just draws the cached copy to the screen.
+ * @param color tint of the curve
+ * @param borderColor the curve border color
+ * @param from index to draw from
+ * @param to index to draw to (exclusive)
+ */
+ public void draw(Color color, Color borderColor, int from, int to) {
+ float alpha = color.a;
+
+ if (fbo == null) {
+ // this should not be null, but issue #106 claims it is possible, at least 3 people had this...
+ // debugging shows that the draw was called after discardGeometry was, which does not really make sense
+ initFBO();
+ }
+
+ if (lastPointDrawn != to || firstPointDrawn != from) {
+ int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
+ int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
+ //glGetInteger requires a buffer of size 16, even though just 4
+ //values are returned in this specific case
+ IntBuffer oldViewport = BufferUtils.createIntBuffer(16);
+ GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport);
+ EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID());
+ GL11.glViewport(0, 0, fbo.width, fbo.height);
+ GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+ this.renderCurve(color, borderColor, from, to, firstPointDrawn != from);
+ lastPointDrawn = to;
+ firstPointDrawn = from;
+ 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));
+ }
+
+ // draw a fullscreen quad with the texture that contains the curve
+ GL11.glEnable(GL11.GL_TEXTURE_2D);
+ GL11.glDisable(GL11.GL_TEXTURE_1D);
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.getTextureID());
+ GL11.glBegin(GL11.GL_QUADS);
+ GL11.glColor4f(1.0f, 1.0f, 1.0f, alpha);
+ GL11.glTexCoord2f(1.0f, 1.0f);
+ GL11.glVertex2i(fbo.width, 0);
+ GL11.glTexCoord2f(0.0f, 1.0f);
+ GL11.glVertex2i(0, 0);
+ GL11.glTexCoord2f(0.0f, 0.0f);
+ GL11.glVertex2i(0, fbo.height);
+ GL11.glTexCoord2f(1.0f, 0.0f);
+ GL11.glVertex2i(fbo.width, fbo.height);
+ GL11.glEnd();
+ }
+
+ /**
+ * Discard the cache mapping for this curve object.
+ */
+ public void discardGeometry() {
+ fbo = null;
+ FrameBufferCache.getInstance().freeMappingFor(hitObject);
+ }
+
+ /**
+ * A structure to hold all the important OpenGL state that needs to be
+ * changed to draw the curve. This is used to backup and restore the state
+ * so that the code outside of this (mainly Slick2D) doesn't break.
+ */
+ private class RenderState {
+ boolean smoothedPoly;
+ boolean blendEnabled;
+ boolean depthEnabled;
+ boolean depthWriteEnabled;
+ boolean texEnabled;
+ int texUnit;
+ int oldProgram;
+ int oldArrayBuffer;
+ }
+
+ /**
+ * Backup the current state of the relevant OpenGL state and change it to
+ * what's needed to draw the curve.
+ */
+ private RenderState saveRenderState() {
+ RenderState state = new RenderState();
+ state.smoothedPoly = GL11.glGetBoolean(GL11.GL_POLYGON_SMOOTH);
+ state.blendEnabled = GL11.glGetBoolean(GL11.GL_BLEND);
+ state.depthEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_TEST);
+ state.depthWriteEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_WRITEMASK);
+ state.texEnabled = GL11.glGetBoolean(GL11.GL_TEXTURE_2D);
+ state.texUnit = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE);
+ state.oldProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
+ state.oldArrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
+ GL11.glDisable(GL11.GL_POLYGON_SMOOTH);
+ GL11.glDisable(GL11.GL_BLEND);
+ GL11.glEnable(GL11.GL_DEPTH_TEST);
+ GL11.glDepthMask(true);
+ GL11.glDisable(GL11.GL_TEXTURE_2D);
+ GL11.glEnable(GL11.GL_TEXTURE_1D);
+ GL11.glBindTexture(GL11.GL_TEXTURE_1D, staticState.gradientTexture);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
+
+ GL20.glUseProgram(0);
+
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ GL11.glPushMatrix();
+ GL11.glLoadIdentity();
+ GL11.glMatrixMode(GL11.GL_MODELVIEW);
+ GL11.glPushMatrix();
+ GL11.glLoadIdentity();
+
+ return state;
+ }
+
+ /**
+ * Restore the old OpenGL state that's backed up in {@code state}.
+ * @param state the old state to restore
+ */
+ private void restoreRenderState(RenderState state) {
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ GL11.glPopMatrix();
+ GL11.glMatrixMode(GL11.GL_MODELVIEW);
+ GL11.glPopMatrix();
+ GL11.glEnable(GL11.GL_BLEND);
+ GL20.glUseProgram(state.oldProgram);
+ GL13.glActiveTexture(state.texUnit);
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, state.oldArrayBuffer);
+ if (!state.depthWriteEnabled)
+ GL11.glDepthMask(false);
+ if (!state.depthEnabled)
+ GL11.glDisable(GL11.GL_DEPTH_TEST);
+ if (state.texEnabled)
+ GL11.glEnable(GL11.GL_TEXTURE_2D);
+ if (state.smoothedPoly)
+ GL11.glEnable(GL11.GL_POLYGON_SMOOTH);
+ if (!state.blendEnabled)
+ GL11.glDisable(GL11.GL_BLEND);
+ }
+
+ /**
+ * Write the vertices and (with position and texture coordinates) for the full
+ * curve into the OpenGL buffer with the ID specified by {@code bufferID}
+ * @param bufferID the buffer ID for the OpenGL buffer the vertices should be written into
+ */
+ private void createVertexBuffer(int bufferID) {
+ int arrayBufferBinding = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
+ FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer();
+ if (curve.length > 0) {
+ fillCone(buff, curve[0].x, curve[0].y);
+ }
+
+ for (int i = 1; i < curve.length; ++i) {
+ float x = curve[i].x;
+ float y = curve[i].y;
+ fillCone(buff, x, y);
+ float last_x = curve[i - 1].x;
+ float last_y = curve[i - 1].y;
+ double diff_x = x - last_x;
+ double diff_y = y - last_y;
+ float dist = Utils.distance(x, y, last_x, last_y);
+ if (dist < Circle.diameter / 8) {
+ x = (float) (x - diff_x / 2);
+ y = (float) (y - diff_y / 2);
+ } else {
+ // don't mind me
+ x = -100f;
+ y = -100f;
+ }
+ fillCone(buff, x, y);
+ }
+ buff.flip();
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferID);
+ GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW);
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, arrayBufferBinding);
+ }
+
+ /**
+ * Do the actual drawing of the curve into the currently bound framebuffer.
+ * @param color the color of the curve
+ * @param borderColor the curve border color
+ */
+ private void renderCurve(Color color, Color borderColor, int from, int to, boolean clearFirst) {
+ staticState.initGradient();
+ RenderState state = saveRenderState();
+ staticState.initShaderProgram();
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, fbo.getVbo());
+ GL20.glUseProgram(staticState.program);
+ GL20.glEnableVertexAttribArray(staticState.attribLoc);
+ GL20.glEnableVertexAttribArray(staticState.texCoordLoc);
+ GL20.glUniform1i(staticState.texLoc, 0);
+ GL20.glUniform3f(staticState.colLoc, color.r, color.g, color.b);
+ GL20.glUniform4f(staticState.colBorderLoc, borderColor.r, borderColor.g, borderColor.b, borderColor.a);
+ //stride is 6*4 for the floats (4 bytes) (u,v)(x,y,z,w)
+ //2*4 is for skipping the first 2 floats (u,v)
+ GL20.glVertexAttribPointer(staticState.attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4);
+ GL20.glVertexAttribPointer(staticState.texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0);
+ if (clearFirst) {
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+ }
+ if (pointsToRender == null) {
+ renderCurve(from, to);
+ } else {
+ renderCurve();
+ }
+ GL11.glFlush();
+ GL20.glDisableVertexAttribArray(staticState.texCoordLoc);
+ GL20.glDisableVertexAttribArray(staticState.attribLoc);
+ restoreRenderState(state);
+ }
+
+ private void renderCurve(int from, int to) {
+ for (int i = from * 2; i < to * 2 - 1; ++i) {
+ if (spliceFrom <= i && i <= spliceTo) {
+ continue;
+ }
+ GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2);
+ }
+ }
+
+ private void renderCurve() {
+ Iterator iter = pointsToRender.iterator();
+ while (iter.hasNext()) {
+ for (int i = iter.next() * 2, end = iter.next() * 2 - 1; i < end; ++i) {
+ GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2);
+ }
+ }
+ }
+
+ /**
+ * Fill {@code buff} with the texture coordinates and positions for a cone
+ * that has its center at the coordinates {@code (x1,y1)}.
+ * @param buff the buffer to be filled
+ * @param x1 x-coordinate of the cone
+ * @param y1 y-coordinate of the cone
+ */
+ protected void fillCone(FloatBuffer buff, float x1, float y1) {
+ float divx = containerWidth / 2.0f;
+ float divy = containerHeight / 2.0f;
+ float offx = -1.0f;
+ float offy = 1.0f;
+ float radius = scale / 2;
+
+ for (int i = 0; i < NewCurveStyleState.unitCone.length / 6; ++i) {
+ buff.put(NewCurveStyleState.unitCone[i * 6 + 0]);
+ buff.put(NewCurveStyleState.unitCone[i * 6 + 1]);
+ buff.put(offx + (x1 + radius * NewCurveStyleState.unitCone[i * 6 + 2]) / divx);
+ buff.put(offy - (y1 + radius * NewCurveStyleState.unitCone[i * 6 + 3]) / divy);
+ buff.put(NewCurveStyleState.unitCone[i * 6 + 4]);
+ buff.put(NewCurveStyleState.unitCone[i * 6 + 5]);
+ }
+ }
+
+ /**
+ * Contains all the necessary state that needs to be tracked to draw curves
+ * in the new style and not re-create the shader each time.
+ *
+ * @author Bigpet {@literal }
+ */
+ private static class NewCurveStyleState {
+ /**
+ * Used for new style Slider rendering, defines how many vertices the
+ * base of the cone has that is used to draw the curve.
+ */
+ protected static final int DIVIDES = 30;
+
+ /**
+ * Array to hold the dummy vertex data (texture coordinates and position)
+ * of a cone with DIVIDES vertices at its base, that is centered around
+ * (0,0) and has a radius of 1 (so that it can be translated and scaled easily).
+ */
+ protected static float[] unitCone = new float[(DIVIDES + 2) * 6];
+
+ /** OpenGL shader program ID used to draw and recolor the curve. */
+ protected int program = 0;
+
+ /** OpenGL shader attribute location of the vertex position attribute. */
+ protected int attribLoc = 0;
+
+ /** OpenGL shader attribute location of the texture coordinate attribute. */
+ protected int texCoordLoc = 0;
+
+ /** OpenGL shader uniform location of the color attribute. */
+ protected int colLoc = 0;
+
+ /** OpenGL shader uniform location of the border color attribute. */
+ protected int colBorderLoc = 0;
+
+ /** OpenGL shader uniform location of the texture sampler attribute. */
+ protected int texLoc = 0;
+
+ /** OpenGL texture id for the gradient texture for the curve. */
+ protected int gradientTexture = 0;
+
+ /**
+ * Reads the first row of the slider gradient texture and upload it as
+ * a 1D texture to OpenGL if it hasn't already been done.
+ */
+ public void initGradient() {
+ if (gradientTexture == 0) {
+ Image slider = GameImage.SLIDER_GRADIENT_EX.getImage().getScaledCopy(1.0f / GameImage.getUIscale());
+ staticState.gradientTexture = GL11.glGenTextures();
+ ByteBuffer buff = BufferUtils.createByteBuffer(slider.getWidth() * 4);
+ for (int i = 0; i < slider.getWidth(); ++i) {
+ Color col = slider.getColor(i, 0);
+ buff.put((byte) (255 * col.r));
+ buff.put((byte) (255 * col.g));
+ buff.put((byte) (255 * col.b));
+ buff.put((byte) (255 * col.a));
+ }
+ buff.flip();
+ GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
+ GL11.glTexImage1D(GL11.GL_TEXTURE_1D, 0, GL11.GL_RGBA, slider.getWidth(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buff);
+ EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D);
+ }
+ }
+
+ /**
+ * Write the data into {@code unitCone} if it hasn't already been initialized.
+ */
+ public static void initUnitCone() {
+ int index = 0;
+ //check if initialization has already happened
+ if (unitCone[0] == 0.0f) {
+ //tip of the cone
+ //vec2 texture coordinates
+ unitCone[index++] = 1.0f;
+ unitCone[index++] = 0.5f;
+
+ //vec4 position
+ unitCone[index++] = 0.0f;
+ unitCone[index++] = 0.0f;
+ unitCone[index++] = 0.0f;
+ unitCone[index++] = 1.0f;
+ for (int j = 0; j < NewCurveStyleState.DIVIDES; ++j) {
+ double phase = j * (float) Math.PI * 2 / NewCurveStyleState.DIVIDES;
+ //vec2 texture coordinates
+ unitCone[index++] = 0.0f;
+ unitCone[index++] = 0.5f;
+ //vec4 positon
+ unitCone[index++] = (float) Math.sin(phase);
+ unitCone[index++] = (float) Math.cos(phase);
+ unitCone[index++] = 1.0f;
+ unitCone[index++] = 1.0f;
+ }
+ //vec2 texture coordinates
+ unitCone[index++] = 0.0f;
+ unitCone[index++] = 0.5f;
+ //vec4 positon
+ unitCone[index++] = (float) Math.sin(0.0f);
+ unitCone[index++] = (float) Math.cos(0.0f);
+ unitCone[index++] = 1.0f;
+ unitCone[index++] = 1.0f;
+ }
+ }
+
+ /**
+ * Compiles and links the shader program for the new style curve objects
+ * if it hasn't already been compiled and linked.
+ */
+ public void initShaderProgram() {
+ if (program == 0) {
+ program = GL20.glCreateProgram();
+ int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
+ int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
+ GL20.glShaderSource(vtxShdr, "#version 110\n"
+ + "\n"
+ + "attribute vec4 in_position;\n"
+ + "attribute vec2 in_tex_coord;\n"
+ + "\n"
+ + "varying vec2 tex_coord;\n"
+ + "void main()\n"
+ + "{\n"
+ + " gl_Position = in_position;\n"
+ + " tex_coord = in_tex_coord;\n"
+ + "}");
+ GL20.glCompileShader(vtxShdr);
+ int res = GL20.glGetShaderi(vtxShdr, GL20.GL_COMPILE_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
+ Log.error("Vertex Shader compilation failed.", new Exception(error));
+ }
+ GL20.glShaderSource(frgShdr, "#version 110\n"
+ + "\n"
+ + "uniform sampler1D tex;\n"
+ + "uniform vec2 tex_size;\n"
+ + "uniform vec3 col_tint;\n"
+ + "uniform vec4 col_border;\n"
+ + "\n"
+ + "varying vec2 tex_coord;\n"
+ + "\n"
+ + "void main()\n"
+ + "{\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"
+ + " gl_FragColor = new_color;\n"
+ + "}");
+ GL20.glCompileShader(frgShdr);
+ res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetShaderInfoLog(frgShdr, 1024);
+ Log.error("Fragment Shader compilation failed.", new Exception(error));
+ }
+ GL20.glAttachShader(program, vtxShdr);
+ GL20.glAttachShader(program, frgShdr);
+ GL20.glLinkProgram(program);
+ res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
+ if (res != GL11.GL_TRUE) {
+ String error = GL20.glGetProgramInfoLog(program, 1024);
+ Log.error("Program linking failed.", new Exception(error));
+ }
+ GL20.glDeleteShader(vtxShdr);
+ GL20.glDeleteShader(frgShdr);
+ attribLoc = GL20.glGetAttribLocation(program, "in_position");
+ texCoordLoc = GL20.glGetAttribLocation(program, "in_tex_coord");
+ texLoc = GL20.glGetUniformLocation(program, "tex");
+ colLoc = GL20.glGetUniformLocation(program, "col_tint");
+ colBorderLoc = GL20.glGetUniformLocation(program, "col_border");
+ }
+ }
+
+ /**
+ * Cleanup any OpenGL objects that may have been initialized.
+ */
+ private void shutdown() {
+ if (gradientTexture != 0) {
+ GL11.glDeleteTextures(gradientTexture);
+ gradientTexture = 0;
+ }
+
+ if (program != 0) {
+ GL20.glDeleteProgram(program);
+ program = 0;
+ attribLoc = 0;
+ texCoordLoc = 0;
+ colLoc = 0;
+ colBorderLoc = 0;
+ texLoc = 0;
+ }
+ }
+ }
+}
diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java
index 83220e61..811f3db0 100644
--- a/src/itdelatrisu/opsu/states/Game.java
+++ b/src/itdelatrisu/opsu/states/Game.java
@@ -42,6 +42,7 @@
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.objects.curves.Curve;
+import itdelatrisu.opsu.objects.curves.FakeCombinedCurve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.options.Options;
import itdelatrisu.opsu.render.FrameBufferCache;
@@ -63,11 +64,7 @@
import java.io.File;
import java.io.IOException;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Stack;
+import java.util.*;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
@@ -319,6 +316,9 @@ public enum Restart {
/** The video seek time (if any). */
private int videoSeekTime;
+ /** The merged slider. */
+ private FakeCombinedCurve mergedslider;
+
/** Music position bar background colors. */
private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
@@ -1201,6 +1201,7 @@ else if (key == Options.getGameKeyRight())
beatmap.objects[objectIndex++].getTime() <= checkpoint)
;
objectIndex--;
+
lastReplayTime = beatmap.objects[objectIndex].getTime();
lastTrackPosition = checkpoint;
} catch (SlickException e) {
@@ -1586,6 +1587,32 @@ else if (hitObject.isSpinner())
MusicController.pause();
SoundController.mute(false);
+
+ if (Options.isMergingSliders()) {
+ if (!Options.isShrinkingSliders()) {
+ mergedslider = null; // workaround for yugecin/opsu-dance#130
+ }
+ if (mergedslider == null) {
+ List curvepoints = new ArrayList<>();
+ for (GameObject gameObject : gameObjects) {
+ if (gameObject instanceof Slider) {
+ ((Slider) gameObject).baseSliderFrom = curvepoints.size();
+ curvepoints.addAll(Arrays.asList(((Slider) gameObject).getCurve().getCurvePoints()));
+ }
+ }
+ if (curvepoints.size() > 0) {
+ mergedslider = new FakeCombinedCurve(curvepoints.toArray(new Vec2f[curvepoints.size()]));
+ }
+ } else {
+ int base = 0;
+ for (GameObject gameObject : gameObjects) {
+ if (gameObject instanceof Slider) {
+ ((Slider) gameObject).baseSliderFrom = base;
+ base += ((Slider) gameObject).getCurve().getCurvePoints().length;
+ }
+ }
+ }
+ }
}
skipButton.resetHover();
@@ -1603,11 +1630,17 @@ public void leave(GameContainer container, StateBasedGame game)
if (GameMod.AUTO.isActive() || isReplay)
UI.getCursor().hide();
+ mergedslider = null;
+
// replays
if (isReplay)
GameMod.loadModState(previousMods);
}
+ public void addMergedSliderPointsToRender(int from, int to) {
+ mergedslider.addRange(from, to);
+ }
+
/**
* Draws hit objects, hit results, and follow points.
* @param g the graphics context
@@ -1617,6 +1650,11 @@ private void drawHitObjects(Graphics g, int trackPosition) {
// draw result objects (under)
data.drawHitResults(trackPosition, false);
+ if (Options.isMergingSliders() && mergedslider != null) {
+ mergedslider.draw(Color.white);
+ mergedslider.initForFrame();
+ }
+
// include previous object in follow points
int lastObjectIndex = -1;
if (objectIndex > 0 && objectIndex < beatmap.objects.length &&