diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs index 4338a1ac77..cf0b6cd7ce 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs @@ -58,28 +58,28 @@ namespace osu.Desktop.VisualTests.Tests { new OsuHitRenderer { - Objects = beatmap.HitObjects, + Beatmap = beatmap, Scale = new Vector2(0.5f), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft }, new TaikoHitRenderer { - Objects = beatmap.HitObjects, + Beatmap = beatmap, Scale = new Vector2(0.5f), Anchor = Anchor.TopRight, Origin = Anchor.TopRight }, new CatchHitRenderer { - Objects = beatmap.HitObjects, + Beatmap = beatmap, Scale = new Vector2(0.5f), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft }, new ManiaHitRenderer { - Objects = beatmap.HitObjects, + Beatmap = beatmap, Scale = new Vector2(0.5f), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight diff --git a/osu.Game.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index f81c88ae02..5b681badd7 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -8,6 +8,7 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.UI; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Catch { @@ -15,7 +16,7 @@ namespace osu.Game.Modes.Catch { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(List objects) => new CatchHitRenderer { Objects = objects }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new CatchHitRenderer { Beatmap = beatmap }; protected override PlayMode PlayMode => PlayMode.Catch; diff --git a/osu.Game.Modes.Catch/Objects/CatchConverter.cs b/osu.Game.Modes.Catch/Objects/CatchConverter.cs index 588cf4780a..d8c2df320f 100644 --- a/osu.Game.Modes.Catch/Objects/CatchConverter.cs +++ b/osu.Game.Modes.Catch/Objects/CatchConverter.cs @@ -4,16 +4,17 @@ using System.Collections.Generic; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Catch.Objects { class CatchConverter : HitObjectConverter { - public override List Convert(List input) + public override List Convert(Beatmap beatmap) { List output = new List(); - foreach (HitObject i in input) + foreach (HitObject i in beatmap.HitObjects) { CatchBaseHit h = i as CatchBaseHit; diff --git a/osu.Game.Modes.Mania/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index 012caf387e..75f7d93228 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Modes.Osu; using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.UI; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Mania { @@ -16,7 +17,7 @@ namespace osu.Game.Modes.Mania { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(List objects) => new ManiaHitRenderer { Objects = objects }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new ManiaHitRenderer { Beatmap = beatmap }; protected override PlayMode PlayMode => PlayMode.Mania; diff --git a/osu.Game.Modes.Mania/Objects/ManiaConverter.cs b/osu.Game.Modes.Mania/Objects/ManiaConverter.cs index 6c95c73b38..654a8fa343 100644 --- a/osu.Game.Modes.Mania/Objects/ManiaConverter.cs +++ b/osu.Game.Modes.Mania/Objects/ManiaConverter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Mania.Objects { @@ -17,11 +18,11 @@ namespace osu.Game.Modes.Mania.Objects this.columns = columns; } - public override List Convert(List input) + public override List Convert(Beatmap beatmap) { List output = new List(); - foreach (HitObject i in input) + foreach (HitObject i in beatmap.HitObjects) { ManiaBaseHit h = i as ManiaBaseHit; diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 3708f429ac..d96726844a 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -28,7 +28,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables osuObject = h; Origin = Anchor.Centre; - Position = osuObject.Position; + Position = osuObject.Position + h.StackOffset; Scale = new Vector2(osuObject.Scale); Children = new Drawable[] diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs index ba67572b72..5587f9c4a2 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs @@ -31,7 +31,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { body = new SliderBody(s) { - Position = s.Position, + Position = s.Position + s.StackOffset, PathWidth = s.Scale * 64, }, bouncer1 = new SliderBouncer(s, false) @@ -41,7 +41,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables }, bouncer2 = new SliderBouncer(s, true) { - Position = s.Position, + Position = s.Position + s.StackOffset, Scale = new Vector2(s.Scale), }, ball = new SliderBall(s) @@ -51,7 +51,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables initialCircle = new DrawableHitCircle(new HitCircle { StartTime = s.StartTime, - Position = s.Position, + Position = s.Position + s.StackOffset, Scale = s.Scale, Colour = s.Colour, Sample = s.Sample, @@ -89,11 +89,11 @@ namespace osu.Game.Modes.Osu.Objects.Drawables if (repeat % 2 == 1) progress = 1 - progress; - bouncer2.Position = slider.Curve.PositionAt(body.SnakedEnd ?? 0); + bouncer2.Position = slider.Curve.PositionAt(body.SnakedEnd ?? 0) + slider.StackOffset; //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. if (initialCircle.Judgement?.Result != HitResult.Hit) - initialCircle.Position = slider.Curve.PositionAt(progress); + initialCircle.Position = slider.Curve.PositionAt(progress) + slider.StackOffset; components.ForEach(c => c.UpdateProgress(progress, repeat)); } diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs index 6791b9dc2c..465751ba97 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -17,6 +17,9 @@ namespace osu.Game.Modes.Osu.Objects public virtual Vector2 EndPosition => Position; + public int StackHeight { get; set; } + public Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f); + public override void SetDefaultsFromBeatmap(Beatmap beatmap) { base.SetDefaultsFromBeatmap(beatmap); diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObjectConverter.cs b/osu.Game.Modes.Osu/Objects/OsuHitObjectConverter.cs index 5a8828ea3f..27376df323 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObjectConverter.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObjectConverter.cs @@ -3,19 +3,168 @@ using System.Collections.Generic; using osu.Game.Modes.Objects; +using osu.Game.Beatmaps; +using osu.Game.Modes.Osu.Objects.Drawables; +using OpenTK; namespace osu.Game.Modes.Osu.Objects { public class OsuHitObjectConverter : HitObjectConverter { - public override List Convert(List input) + public override List Convert(Beatmap beatmap) { List output = new List(); - foreach (HitObject h in input) + foreach (HitObject h in beatmap.HitObjects) output.Add(h as OsuHitObject); + UpdateStacking(output, beatmap.BeatmapInfo?.StackLeniency ?? 0.7f); + return output; } + + public static void UpdateStacking(List hitObjects, float stackLeniency, int startIndex = 0, int endIndex = -1) + { + if (endIndex == -1) + endIndex = hitObjects.Count - 1; + + int stackDistance = 3; + float stackThreshold = DrawableOsuHitObject.TIME_PREEMPT * stackLeniency; + + // Reset stacking inside the update range + for (int i = startIndex; i <= endIndex; i++) + hitObjects[i].StackHeight = 0; + + // Extend the end index to include objects they are stacked on + int extendedEndIndex = endIndex; + for (int i = endIndex; i >= startIndex; i--) + { + int stackBaseIndex = i; + for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++) + { + OsuHitObject stackBaseObject = hitObjects[stackBaseIndex]; + if (stackBaseObject is Spinner) break; + + OsuHitObject objectN = hitObjects[n]; + if (objectN is Spinner) continue; + + if (objectN.StartTime - stackBaseObject.EndTime > stackThreshold) + //We are no longer within stacking range of the next object. + break; + + if (Vector2.Distance(stackBaseObject.Position, objectN.Position) < stackDistance || + (stackBaseObject is Slider && Vector2.Distance(stackBaseObject.EndPosition, objectN.Position) < stackDistance)) + { + stackBaseIndex = n; + + // HitObjects after the specified update range haven't been reset yet + objectN.StackHeight = 0; + } + } + + if (stackBaseIndex > extendedEndIndex) + { + extendedEndIndex = stackBaseIndex; + if (extendedEndIndex == hitObjects.Count - 1) + break; + } + } + + //Reverse pass for stack calculation. + int extendedStartIndex = startIndex; + for (int i = extendedEndIndex; i > startIndex; i--) + { + int n = i; + /* We should check every note which has not yet got a stack. + * Consider the case we have two interwound stacks and this will make sense. + * + * o <-1 o <-2 + * o <-3 o <-4 + * + * We first process starting from 4 and handle 2, + * then we come backwards on the i loop iteration until we reach 3 and handle 1. + * 2 and 1 will be ignored in the i loop because they already have a stack value. + */ + + OsuHitObject objectI = hitObjects[i]; + if (objectI.StackHeight != 0 || objectI is Spinner) continue; + + /* If this object is a hitcircle, then we enter this "special" case. + * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. + * Any other case is handled by the "is Slider" code below this. + */ + if (objectI is HitCircle) + { + while (--n >= 0) + { + OsuHitObject objectN = hitObjects[n]; + if (objectN is Spinner) continue; + + if (objectI.StartTime - objectN.EndTime > stackThreshold) + //We are no longer within stacking range of the previous object. + break; + + // HitObjects before the specified update range haven't been reset yet + if (n < extendedStartIndex) + { + objectN.StackHeight = 0; + extendedStartIndex = n; + } + + /* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern. + * o==o <- slider is at original location + * o <- hitCircle has stack of -1 + * o <- hitCircle has stack of -2 + */ + if (objectN is Slider && Vector2.Distance(objectN.EndPosition, objectI.Position) < stackDistance) + { + int offset = objectI.StackHeight - objectN.StackHeight + 1; + for (int j = n + 1; j <= i; j++) + { + //For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above). + OsuHitObject objectJ = hitObjects[j]; + if (Vector2.Distance(objectN.EndPosition, objectJ.Position) < stackDistance) + objectJ.StackHeight -= offset; + } + + //We have hit a slider. We should restart calculation using this as the new base. + //Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop. + break; + } + + if (Vector2.Distance(objectN.Position, objectI.Position) < stackDistance) + { + //Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out. + //NOTE: Sliders with start positions stacking are a special case that is also handled here. + + objectN.StackHeight = objectI.StackHeight + 1; + objectI = objectN; + } + } + } + else if (objectI is Slider) + { + /* We have hit the first slider in a possible stack. + * From this point on, we ALWAYS stack positive regardless. + */ + while (--n >= startIndex) + { + OsuHitObject objectN = hitObjects[n]; + if (objectN is Spinner) continue; + + if (objectI.StartTime - objectN.StartTime > stackThreshold) + //We are no longer within stacking range of the previous object. + break; + + if (Vector2.Distance(objectN.EndPosition, objectI.Position) < stackDistance) + { + objectN.StackHeight = objectI.StackHeight + 1; + objectI = objectN; + } + } + } + } + } + } } diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index 0276f5f90f..398060aa73 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -16,7 +16,7 @@ namespace osu.Game.Modes.Osu { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(List objects) => new OsuHitRenderer { Objects = objects }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new OsuHitRenderer { Beatmap = beatmap }; public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] { diff --git a/osu.Game.Modes.Taiko/Objects/TaikoConverter.cs b/osu.Game.Modes.Taiko/Objects/TaikoConverter.cs index 801648a89f..5eb69b62ce 100644 --- a/osu.Game.Modes.Taiko/Objects/TaikoConverter.cs +++ b/osu.Game.Modes.Taiko/Objects/TaikoConverter.cs @@ -4,16 +4,17 @@ using System.Collections.Generic; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Taiko.Objects { class TaikoConverter : HitObjectConverter { - public override List Convert(List input) + public override List Convert(Beatmap beatmap) { List output = new List(); - foreach (HitObject i in input) + foreach (HitObject i in beatmap.HitObjects) { TaikoBaseHit h = i as TaikoBaseHit; diff --git a/osu.Game.Modes.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index c7e0e974a8..c5d5ff5805 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.Taiko.UI; using osu.Game.Modes.UI; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Taiko { @@ -16,7 +17,7 @@ namespace osu.Game.Modes.Taiko { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(List objects) => new TaikoHitRenderer { Objects = objects }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new TaikoHitRenderer { Beatmap = beatmap }; protected override PlayMode PlayMode => PlayMode.Taiko; diff --git a/osu.Game/Modes/Objects/HitObjectConverter.cs b/osu.Game/Modes/Objects/HitObjectConverter.cs index da0f93880c..d7c6113af1 100644 --- a/osu.Game/Modes/Objects/HitObjectConverter.cs +++ b/osu.Game/Modes/Objects/HitObjectConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; using System; using System.Collections.Generic; @@ -9,7 +10,7 @@ namespace osu.Game.Modes.Objects public abstract class HitObjectConverter where T : HitObject { - public abstract List Convert(List input); + public abstract List Convert(Beatmap beatmap); } public class HitObjectConvertException : Exception diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 4739f8ba56..5b9373c10b 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -28,7 +28,7 @@ namespace osu.Game.Modes public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount); - public abstract HitRenderer CreateHitRendererWith(List objects); + public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap); public abstract HitObjectParser CreateHitObjectParser(); diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index c69cdd1a18..aa2af83cb4 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; +using osu.Game.Beatmaps; namespace osu.Game.Modes.UI { @@ -37,7 +38,7 @@ namespace osu.Game.Modes.UI { private List objects; - public List Objects + public Beatmap Beatmap { set { @@ -51,7 +52,7 @@ namespace osu.Game.Modes.UI protected abstract HitObjectConverter Converter { get; } - protected virtual List Convert(List objects) => Converter.Convert(objects); + protected virtual List Convert(Beatmap beatmap) => Converter.Convert(beatmap); public HitRenderer() { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2913c76213..a9584a1f10 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Play OnQuit = Exit }; - hitRenderer = ruleset.CreateHitRendererWith(beatmap.HitObjects); + hitRenderer = ruleset.CreateHitRendererWith(beatmap); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) hitRenderer.OnJudgement += scoreProcessor.AddJudgement;