1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:17:26 +08:00

Merge pull request #24280 from tybug/stable-notelock

Correctly implement stable notelock in Classic mod
This commit is contained in:
Dean Herbert 2023-09-01 18:05:57 +09:00 committed by GitHub
commit 8d0f6df329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 294 additions and 91 deletions

View File

@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
{
// force success
ApplyResult(r => r.Type = HitResult.Great);

View File

@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (shouldHit && !userTriggered && timeOffset >= 0 && CheckHittable?.Invoke(this, Time.Current) != false)
if (shouldHit && !userTriggered && timeOffset >= 0)
{
// force success
ApplyResult(r => r.Type = HitResult.Great);

View File

@ -11,17 +11,21 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -32,7 +36,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
{
private readonly OsuHitWindows referenceHitWindows;
@ -43,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
/// </summary>
private readonly string? exportLocation = null;
public TestSceneObjectOrderedHitPolicy()
public TestSceneLegacyHitPolicy()
{
referenceHitWindows = new OsuHitWindows();
referenceHitWindows.SetDifficulty(0);
@ -83,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Miss);
// note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Shake);
}
/// <summary>
@ -119,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Miss);
// note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Shake);
}
/// <summary>
@ -155,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Miss);
// note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Shake);
}
/// <summary>
@ -191,7 +198,9 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
/// <summary>
@ -229,13 +238,15 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
/// <summary>
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
/// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks.
/// </summary>
[Test]
public void TestMissSliderHeadAndHitAllSliderTicks()
public void TestHitCircleBeforeSliderHead()
{
const double time_slider = 1500;
const double time_circle = 1510;
@ -267,10 +278,12 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
/// <summary>
@ -314,6 +327,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
/// <summary>
@ -353,6 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addClickActionAssert(0, ClickAction.Hit);
}
[Test]
@ -391,6 +407,102 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addClickActionAssert(0, ClickAction.Shake);
addClickActionAssert(1, ClickAction.Hit);
addClickActionAssert(2, ClickAction.Hit);
}
[Test]
public void TestOverlappingSliders()
{
const double time_first_slider = 1000;
const double time_second_slider = 1200;
Vector2 positionFirstSlider = new Vector2(100, 50);
Vector2 positionSecondSlider = new Vector2(100, 80);
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
var hitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
},
new Slider
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } },
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementAssert(hitObjects[1], HitResult.Great);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
[Test]
public void TestStacksDoNotShake()
{
const double time_stack_start = 1000;
Vector2 position = new Vector2(80);
var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle
{
StartTime = time_stack_start + i * 100,
Position = position
}).Cast<OsuHitObject>().ToList();
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
});
addClickActionAssert(0, ClickAction.Ignore);
}
[Test]
public void TestAutopilotReducesHittableRange()
{
const double time_circle = 1500;
Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_circle,
Position = positionCircle
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } }
}, new Mod[] { new OsuModAutopilot() });
addJudgementAssert(hitObjects[0], HitResult.Miss);
// note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Shake);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
@ -408,17 +520,30 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100));
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
}
private void addClickActionAssert(int inputIndex, ClickAction action)
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private List<JudgementResult> judgementResults = null!;
private TestLegacyHitPolicy testPolicy = null!;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, [CallerMemberName] string testCaseName = "")
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, IEnumerable<Mod>? extraMods = null, [CallerMemberName] string testCaseName = "")
{
List<Mod> mods = null!;
IBeatmap playableBeatmap = null!;
Score score = null!;
AddStep("set up mods", () =>
{
mods = new List<Mod> { new OsuModClassic() };
if (extraMods != null)
mods.AddRange(extraMods);
});
AddStep("create beatmap", () =>
{
var cpi = new ControlPointInfo();
@ -461,7 +586,8 @@ namespace osu.Game.Rulesets.Osu.Tests
ScoreInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = playableBeatmap.BeatmapInfo
BeatmapInfo = playableBeatmap.BeatmapInfo,
Mods = mods.ToArray()
}
};
});
@ -495,7 +621,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load player", () =>
{
SelectedMods.Value = new[] { new OsuModClassic() };
SelectedMods.Value = mods.ToArray();
var p = new ScoreAccessibleReplayPlayer(score);
@ -513,6 +639,12 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddStep("Substitute hit policy", () =>
{
var playfield = currentPlayer.ChildrenOfType<OsuPlayfield>().Single();
var currentPolicy = playfield.HitPolicy;
playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy);
});
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
@ -540,5 +672,24 @@ namespace osu.Game.Rulesets.Osu.Tests
{
}
}
private class TestLegacyHitPolicy : LegacyHitPolicy
{
private readonly IHitPolicy currentPolicy;
public TestLegacyHitPolicy(IHitPolicy currentPolicy)
{
this.currentPolicy = currentPolicy;
}
public List<ClickAction> ClickActions { get; } = new List<ClickAction>();
public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
{
var action = currentPolicy.CheckHittable(hitObject, time, result);
ClickActions.Add(action);
return action;
}
}
}
}

View File

@ -11,6 +11,7 @@ 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.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@ -57,7 +58,10 @@ namespace osu.Game.Rulesets.Osu.Mods
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
{
double hittableRange = OsuHitWindows.MISS_WINDOW - (drawableRuleset.Mods.OfType<OsuModAutopilot>().Any() ? 200 : 0);
osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(hittableRange);
}
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
}

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
var result = ResultFor(timeOffset);
var clickAction = CheckHittable?.Invoke(this, Time.Current, result);
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
if (clickAction == ClickAction.Shake)
Shake();
if (result == HitResult.None || clickAction != ClickAction.Hit)
return;
}
ApplyResult(r =>
{

View File

@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
/// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
/// click at the given time value.
/// If non-null, judgements will be ignored for return values of <see cref="ClickAction.Ignore"/>
/// and <see cref="ClickAction.Shake"/>, and this hit object will be shaken for return values of
/// <see cref="ClickAction.Shake"/>.
/// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable;
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)

View File

@ -8,6 +8,7 @@ using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindTo(DrawableSlider.PathVersion);
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
}
protected override void Update()

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
public class AnyOrderHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public IHitObjectContainer HitObjectContainer { get; set; } = null!;
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) => ClickAction.Hit;
public void HandleHit(DrawableHitObject hitObject)
{

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// An action that an <see cref="IHitPolicy"/> recommends be taken in response to a click
/// on a <see cref="DrawableOsuHitObject"/>.
/// </summary>
public enum ClickAction
{
Ignore,
Shake,
Hit
}
}

View File

@ -3,6 +3,7 @@
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <param name="result">The result that the object would be judged with if hit.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
bool IsHittable(DrawableHitObject hitObject, double time);
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
/// <summary>
/// Handles a <see cref="HitObject"/> being hit.

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
/// <remarks>
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
/// </remarks>
/// </summary>
public class LegacyHitPolicy : IHitPolicy
{
public IHitObjectContainer? HitObjectContainer { get; set; }
private readonly double hittableRange;
public LegacyHitPolicy(double hittableRange = OsuHitWindows.MISS_WINDOW)
{
this.hittableRange = hittableRange;
}
public void HandleHit(DrawableHitObject hitObject)
{
}
public virtual ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
{
if (HitObjectContainer == null)
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
var aliveObjects = HitObjectContainer.AliveObjects.ToList();
int index = aliveObjects.IndexOf(hitObject);
if (index > 0)
{
var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1];
if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged)
return ClickAction.Ignore;
}
if (result == HitResult.None)
return ClickAction.Shake;
foreach (DrawableHitObject testObject in aliveObjects)
{
if (testObject.AllJudged)
continue;
// if we found the object being checked, we can move on to the final timing test.
if (testObject == hitObject)
break;
// for all other objects, we check for validity and block the hit if any are still valid.
// 3ms of extra leniency to account for slightly unsnapped objects.
if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime)
return ClickAction.Shake;
}
return Math.Abs(hitObject.HitObject.StartTime - time) < hittableRange ? ClickAction.Hit : ClickAction.Shake;
}
}
}

View File

@ -1,56 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
/// <remarks>
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
/// </remarks>
/// </summary>
public class ObjectOrderedHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
public void HandleHit(DrawableHitObject hitObject)
{
}
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in HitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
switch (obj)
{
case DrawableSpinner:
continue;
case DrawableSlider slider:
yield return slider.HeadCircle;
break;
default:
yield return obj;
break;
}
}
}
}
}

View File

@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
drawable.OnLoadComplete += onDrawableHitObjectLoaded;

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
public class StartTimeOrderedHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public IHitObjectContainer? HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time)
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult _)
{
DrawableHitObject blockingObject = null;
if (HitObjectContainer == null)
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
DrawableHitObject? blockingObject = null;
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
// If there is no previous hitobject, allow the hit.
if (blockingObject == null)
return true;
return ClickAction.Hit;
// 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).
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
}
public void HandleHit(DrawableHitObject hitObject)
{
if (HitObjectContainer == null)
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.");
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
if (!hitObjectCanBlockFutureHits(hitObject))
return;
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
// Miss all hitobjects prior to the hit one.
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in HitObjectContainer.AliveObjects)
foreach (var obj in HitObjectContainer!.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;