diff --git a/osu.Android.props b/osu.Android.props
index aaac6ec427..5b200ee104 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index f1750f4a01..d569d68b59 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
new Container
{
RelativeSizeAxes = Axes.Both,
+ Masking = true,
BorderThickness = 1,
BorderColour = colours.Yellow,
Child = new Box
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
new file mode 100644
index 0000000000..d6858f831e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -0,0 +1,412 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
+ {
+ private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
+ private const double late_miss_window = 500; // time after +500 is considered a miss
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAtFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], 0);
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], 100);
+ }
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
+ }
+
+ ///
+ /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
+ ///
+ [Test]
+ public void TestMissSliderHeadAndHitAllSliderTicks()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ }
+
+ ///
+ /// Tests clicking hitting future slider ticks before a circle.
+ ///
+ [Test]
+ public void TestHitSliderTicksBeforeCircle()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ }
+
+ ///
+ /// Tests clicking a future circle before a spinner.
+ ///
+ [Test]
+ public void TestHitCircleBeforeSpinner()
+ {
+ const double time_spinner = 1500;
+ const double time_circle = 1800;
+ Vector2 positionCircle = Vector2.Zero;
+
+ var hitObjects = new List
+ {
+ new TestSpinner
+ {
+ StartTime = time_spinner,
+ Position = new Vector2(256, 192),
+ EndTime = time_spinner + 1000,
+ },
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementAssert(string name, Func hitObject, HitResult result)
+ {
+ AddAssert($"{name} judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+ private bool allJudgedFired;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+ Ruleset = new OsuRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ p.ScoreProcessor.AllJudged += () =>
+ {
+ if (currentPlayer == p) allJudgedFired = true;
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ allJudgedFired = false;
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for all judged", () => allJudgedFired);
+ }
+
+ private class TestHitCircle : HitCircle
+ {
+ protected override HitWindows CreateHitWindows() => new TestHitWindows();
+ }
+
+ private class TestSlider : Slider
+ {
+ public TestSlider()
+ {
+ DefaultsApplied += () =>
+ {
+ HeadCircle.HitWindows = new TestHitWindows();
+ TailCircle.HitWindows = new TestHitWindows();
+ };
+ }
+ }
+
+ private class TestSpinner : Spinner
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ SpinsRequired = 1;
+ }
+ }
+
+ private class TestHitWindows : HitWindows
+ {
+ private static readonly DifficultyRange[] ranges =
+ {
+ new DifficultyRange(HitResult.Great, 500, 500, 500),
+ new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
+ };
+
+ public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
+
+ protected override DifficultyRange[] GetRanges() => ranges;
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 5202327245..d73ad888f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset);
- if (result == HitResult.None)
+ if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
return;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index a677cb6a72..fe23e3729d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -16,6 +18,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
public override bool HandlePositionalInput => true;
+ ///
+ /// Whether this can be hit.
+ /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
+ ///
+ public Func CheckHittable;
+
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@@ -54,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ ///
+ /// Causes this to get missed, disregarding all conditions in implementations of .
+ ///
+ public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9b6f39d91d..522217a916 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case SliderTailCircle tail:
return new DrawableSliderTail(slider, tail);
- case HitCircle head:
+ case SliderHeadCircle head:
return new DrawableSliderHead(slider, head) { OnShake = Shake };
case SliderTick tick:
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index a360071f26..04f563eeec 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Slider slider;
- public DrawableSliderHead(Slider slider, HitCircle h)
+ public DrawableSliderHead(Slider slider, SliderHeadCircle h)
: base(h)
{
this.slider = slider;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index db1f46d8e2..e5d6c20738 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Head:
- AddNested(HeadCircle = new SliderCircle
+ AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
new file mode 100644
index 0000000000..f6d46aeef5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
@@ -0,0 +1,9 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SliderHeadCircle : HitCircle
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
new file mode 100644
index 0000000000..dfca2aff7b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
@@ -0,0 +1,127 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Ensures that s are hit in-order.
+ /// If a is hit out of order:
+ ///
+ /// - The hit is blocked if it occurred earlier than the previous 's start time.
+ /// - The hit causes all previous s to missed otherwise.
+ ///
+ ///
+ public class OrderedHitPolicy
+ {
+ private readonly HitObjectContainer hitObjectContainer;
+
+ public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+ }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ public bool IsHittable(DrawableHitObject hitObject, double time)
+ {
+ DrawableHitObject blockingObject = null;
+
+ // Find the last hitobject which blocks future hits.
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj == hitObject)
+ break;
+
+ if (drawableCanBlockFutureHits(obj))
+ blockingObject = obj;
+ }
+
+ // If there is no previous hitobject, allow the hit.
+ if (blockingObject == null)
+ return true;
+
+ // A hit is allowed if:
+ // 1. The last blocking hitobject has been judged.
+ // 2. The current time is after the last hitobject's start time.
+ // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
+ if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime)
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Handles a being hit to potentially miss all earlier s.
+ ///
+ /// The that was hit.
+ public void HandleHit(HitObject hitObject)
+ {
+ // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
+ if (!hitObjectCanBlockFutureHits(hitObject))
+ return;
+
+ double maximumTime = hitObject.StartTime;
+
+ // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits.
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.Judged || obj.HitObject.StartTime >= maximumTime)
+ continue;
+
+ if (hitObjectCanBlockFutureHits(obj.HitObject))
+ applyMiss(obj);
+
+ foreach (var nested in obj.NestedHitObjects)
+ {
+ if (nested.Judged || nested.HitObject.StartTime >= maximumTime)
+ continue;
+
+ if (hitObjectCanBlockFutureHits(nested.HitObject))
+ applyMiss(nested);
+ }
+ }
+
+ static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully();
+ }
+
+ ///
+ /// Whether a blocks hits on future s until its start time is reached.
+ ///
+ ///
+ /// This will ONLY match on top-most s.
+ ///
+ /// The to test.
+ private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject)
+ {
+ // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over.
+ return hitObject is DrawableHitCircle || hitObject is DrawableSlider;
+ }
+
+ ///
+ /// Whether a blocks hits on future s until its start time is reached.
+ ///
+ ///
+ /// This is more rigorous and may not match on top-most s as does.
+ ///
+ /// The to test.
+ private static bool hitObjectCanBlockFutureHits(HitObject hitObject)
+ {
+ // Unlike the above we will receive slider tails, but they do not block future hits.
+ if (hitObject is SliderTailCircle)
+ return false;
+
+ // All other hitcircles continue to block future hits.
+ return hitObject is HitCircle;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 6d1ea4bbfc..2f222f59b4 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
+ private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -51,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI
Depth = -1,
},
};
+
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
}
public override void Add(DrawableHitObject h)
@@ -64,7 +67,10 @@ namespace osu.Game.Rulesets.Osu.UI
base.Add(h);
- followPoints.AddFollowPoints((DrawableOsuHitObject)h);
+ DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
+ osuHitObject.CheckHittable = hitPolicy.IsHittable;
+
+ followPoints.AddFollowPoints(osuHitObject);
}
public override bool Remove(DrawableHitObject h)
@@ -79,6 +85,9 @@ namespace osu.Game.Rulesets.Osu.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
+ // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
+ hitPolicy.HandleHit(result.HitObject);
+
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionsDropdown.cs
index 4f59b053b6..5bd321f31e 100644
--- a/osu.Game/Overlays/Music/CollectionsDropdown.cs
+++ b/osu.Game/Overlays/Music/CollectionsDropdown.cs
@@ -29,14 +29,8 @@ namespace osu.Game.Overlays.Music
{
public CollectionsMenu()
{
+ Masking = true;
CornerRadius = 5;
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Colour = Color4.Black.Opacity(0.3f),
- Radius = 3,
- Offset = new Vector2(0f, 1f),
- };
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0011faefbb..8fa0c041d4 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
- protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => AllJudged && base.ComputeIsMaskedAway(maskingBounds);
+ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds);
protected override void UpdateAfterChildren()
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 3e2c2b1599..7cf1272611 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7903d964ce..c58a431e80 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -79,7 +79,7 @@
-
+