diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEndMissJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEndMissJudgement.cs new file mode 100644 index 000000000000..317edce7245e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEndMissJudgement.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +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.Configuration; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +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 partial class TestSceneSliderEndMissJudgement : RateAdjustedBeatmapTestScene + { + private const double time_slider_start = 1000; + private const float slider_path_length = 200; + + private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192); + + private OsuRulesetConfigManager config = null!; + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private readonly List judgementResults = new List(); + + [BackgroundDependencyLoader] + private void load() + { + config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(new OsuRuleset()).AsNonNull(); + } + + [Test] + public void TestMissMarkersHidden() + { + AddStep("disable slider tick miss markers", () => config.SetValue(OsuRulesetSetting.ShowSliderTickMissMarkers, false)); + + performMiss(); + + AddUntilStep("wait for tick miss", () => judgementResults.Any(r => r.Type == HitResult.LargeTickMiss)); + AddAssert("no slider miss markers shown", () => !currentPlayer.ChildrenOfType() + .Any(j => j.Result?.Type is HitResult.LargeTickMiss or HitResult.IgnoreMiss)); + } + + [Test] + public void TestMissMarkersShown() + { + AddStep("enable slider tick miss markers", () => config.SetValue(OsuRulesetSetting.ShowSliderTickMissMarkers, true)); + + performMiss(); + + AddUntilStep("slider miss markers shown", () => currentPlayer.ChildrenOfType() + .Any(j => j.Result?.Type is HitResult.LargeTickMiss or HitResult.IgnoreMiss)); + } + + private void performMiss() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 50, slider_start_position), + }); + } + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Slider + { + StartTime = time_slider_start, + Position = slider_start_position, + TickDistanceMultiplier = 3, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(slider_path_length, 0), + }, slider_path_length), + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderMultiplier = 1, + SliderTickRate = 3, + OverallDifficulty = 0 + }, + Ruleset = new OsuRuleset().RulesetInfo, + } + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 1bfc6bf1ee93..3fe8cb0d759a 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -19,6 +19,7 @@ protected override void InitialiseDefaults() base.InitialiseDefaults(); SetDefault(OsuRulesetSetting.SnakingInSliders, true); SetDefault(OsuRulesetSetting.SnakingOutSliders, true); + SetDefault(OsuRulesetSetting.ShowSliderTickMissMarkers, true); SetDefault(OsuRulesetSetting.HitAnimations, true); SetDefault(OsuRulesetSetting.ShowCursorTrail, true); SetDefault(OsuRulesetSetting.ShowCursorRipples, false); @@ -36,6 +37,7 @@ public enum OsuRulesetSetting { SnakingInSliders, SnakingOutSliders, + ShowSliderTickMissMarkers, HitAnimations, ShowCursorTrail, ShowCursorRipples, diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e379c4431468..562807287fc6 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -52,6 +53,8 @@ public partial class OsuPlayfield : Playfield private readonly Container judgementAboveHitObjectLayer; + private readonly Bindable showSliderTickMissMarkers = new Bindable(true); + public OsuPlayfield() { Anchor = Anchor.Centre; @@ -135,6 +138,7 @@ private void onJudgementLoaded(DrawableOsuJudgement judgement) private void load(OsuRulesetConfigManager? config, IBeatmap? beatmap) { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); + config?.BindWith(OsuRulesetSetting.ShowSliderTickMissMarkers, showSliderTickMissMarkers); var osuBeatmap = (OsuBeatmap?)beatmap; @@ -194,6 +198,13 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; + // LargeTickMiss = slider ticks / ends / repeats; IgnoreMiss = slider tails. + if (!showSliderTickMissMarkers.Value + && result.Type is HitResult.LargeTickMiss or HitResult.IgnoreMiss) + { + return; + } + var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject)); if (explosion == null) diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 8eb804d69336..daa87fb013ad 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -43,6 +43,11 @@ private void load() ApplyClassicDefault = c => ((IHasCurrentValue)c).Current.Value = false, }, new SettingsItemV2(new FormCheckBox + { + Caption = RulesetSettingsStrings.SliderTickMissMarkers, + Current = config.GetBindable(OsuRulesetSetting.ShowSliderTickMissMarkers) + }), + new SettingsItemV2(new FormCheckBox { Caption = RulesetSettingsStrings.HitAnimations, HintText = RulesetSettingsStrings.HitAnimationsOsuTooltip, diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index a57068cbd26e..006c199224c3 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -24,6 +24,11 @@ public static class RulesetSettingsStrings /// public static LocalisableString SnakingOutSliders => new TranslatableString(getKey(@"snaking_out_sliders"), @"Snaking out sliders"); + /// + /// "Slider tick miss markers" + /// + public static LocalisableString SliderTickMissMarkers => new TranslatableString(getKey(@"slider_tick_miss_markers"), @"Slider tick miss markers"); + /// /// "Cursor trail" ///