diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7cd06c5225..79d575ab3f 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.5295339534769958d, "diffcalc-test")] - [TestCase(1.1514260533755143d, "zero-length-sliders")] + [TestCase(6.531832890435525d, "diffcalc-test")] + [TestCase(1.4644923495008817d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(9.047752485219954d, "diffcalc-test")] - [TestCase(1.3985711787077566d, "zero-length-sliders")] + [TestCase(8.8067616302940852d, "diffcalc-test")] + [TestCase(1.7763214959309293d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 4b90285fd4..cbaad93bed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -14,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. private const int min_delta_time = 25; + private const float maximum_slider_radius = normalized_radius * 2.4f; + private const float assumed_slider_radius = normalized_radius * 1.65f; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; @@ -89,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (lastObject is Slider lastSlider) { computeSliderCursorPosition(lastSlider); - TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; + TravelDistance = lastSlider.LazyTravelDistance; TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time); @@ -99,7 +100,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // For hitobjects which continue in the direction of the slider, the player will normally follow through the slider, // such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider. // In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance. - MovementDistance = Math.Min(JumpDistance, tailJumpDistance); + // Additional distance is removed based on position of jump relative to slider follow circle radius. + // JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible. + MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } else { @@ -126,37 +129,60 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (slider.LazyEndPosition != null) return; - slider.LazyEndPosition = slider.StackedPosition; + slider.LazyTravelTime = slider.NestedHitObjects[^1].StartTime - slider.StartTime; - float followCircleRadius = (float)(slider.Radius * 2.4); - var computeVertex = new Action(t => + double endTimeMin = slider.LazyTravelTime / slider.SpanDuration; + if (endTimeMin % 2 >= 1) + endTimeMin = 1 - endTimeMin % 1; + else + endTimeMin %= 1; + + slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. + var currCursorPosition = slider.StackedPosition; + double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. + + for (int i = 1; i < slider.NestedHitObjects.Count; i++) { - double progress = (t - slider.StartTime) / slider.SpanDuration; - if (progress % 2 >= 1) - progress = 1 - progress % 1; - else - progress %= 1; + var currMovementObj = (OsuHitObject)slider.NestedHitObjects[i]; - // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version) - var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value; - float dist = diff.Length; + Vector2 currMovement = Vector2.Subtract(currMovementObj.StackedPosition, currCursorPosition); + double currMovementLength = scalingFactor * currMovement.Length; - slider.LazyTravelTime = t - slider.StartTime; + // Amount of movement required so that the cursor position needs to be updated. + double requiredMovement = assumed_slider_radius; - if (dist > followCircleRadius) + if (i == slider.NestedHitObjects.Count - 1) { - // The cursor would be outside the follow circle, we need to move it - diff.Normalize(); // Obtain direction of diff - dist -= followCircleRadius; - slider.LazyEndPosition += diff * dist; - slider.LazyTravelDistance += dist; - } - }); + // The end of a slider has special aim rules due to the relaxed time constraint on position. + // There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement. + // For sliders that are circular, the lazy end position may actually be farther away than the sliders true end. + // This code is designed to prevent buffing situations where lazy end is actually a less efficient movement. + Vector2 lazyMovement = Vector2.Subtract((Vector2)slider.LazyEndPosition, currCursorPosition); - // Skip the head circle - var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime); - foreach (double time in scoringTimes) - computeVertex(time); + if (lazyMovement.Length < currMovement.Length) + currMovement = lazyMovement; + + currMovementLength = scalingFactor * currMovement.Length; + } + else if (currMovementObj is SliderRepeat) + { + // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. + requiredMovement = normalized_radius; + } + + if (currMovementLength > requiredMovement) + { + // this finds the positional delta from the required radius and the current position, and updates the currCursorPosition accordingly, as well as rewarding distance. + currCursorPosition = Vector2.Add(currCursorPosition, Vector2.Multiply(currMovement, (float)((currMovementLength - requiredMovement) / currMovementLength))); + currMovementLength *= (currMovementLength - requiredMovement) / currMovementLength; + slider.LazyTravelDistance += (float)currMovementLength; + } + + if (i == slider.NestedHitObjects.Count - 1) + slider.LazyEndPosition = currCursorPosition; + } + + slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved. } private Vector2 getEndCursorPosition(OsuHitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index a054b46366..e64cdbfa1d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double wide_angle_multiplier = 1.5; private const double acute_angle_multiplier = 2.0; + private const double slider_multiplier = 1.5; private double currentStrain = 1; @@ -62,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } double angleBonus = 0; + double sliderBonus = 0; + double aimStrain = currVelocity; // Start strain with regular velocity. if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. @@ -91,11 +94,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the angle buffs together. + angleBonus = Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier); // Take the max of the multipliers. } } + if (osuCurrObj.TravelTime != 0) + { + sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // add some slider rewards + } + aimStrain += angleBonus; // Add in angle bonus. + aimStrain += sliderBonus * slider_multiplier; // Add in additional slider velocity. return aimStrain; } diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs index 84cf796835..fd620a0e95 100644 --- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs +++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs @@ -105,6 +105,9 @@ namespace osu.Game.Tests.Mods testMod.ResetSettingsToDefaults(); Assert.That(testMod.DrainRate.Value, Is.Null); + + // ReSharper disable once HeuristicUnreachableCode + // see https://youtrack.jetbrains.com/issue/RIDER-70159. Assert.That(testMod.OverallDifficulty.Value, Is.Null); var applied = applyDifficulty(new BeatmapDifficulty diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs index 97105b6b6a..8386a10ebb 100644 --- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs +++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs @@ -79,8 +79,17 @@ namespace osu.Game.Tests.NonVisual public List HitObjects; public override IEnumerable Objects => HitObjects; - public override event Action NewResult; - public override event Action RevertResult; + public override event Action NewResult + { + add => throw new InvalidOperationException(); + remove => throw new InvalidOperationException(); + } + + public override event Action RevertResult + { + add => throw new InvalidOperationException(); + remove => throw new InvalidOperationException(); + } public override Playfield Playfield { get; } public override Container Overlays { get; } @@ -95,9 +104,6 @@ namespace osu.Game.Tests.NonVisual public TestDrawableRuleset() : base(new OsuRuleset()) { - // won't compile without this. - NewResult?.Invoke(null); - RevertResult?.Invoke(null); } public override void SetReplayScore(Score replayScore) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 1ba0965ceb..c1260f0231 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -235,8 +235,17 @@ namespace osu.Game.Tests.Visual.Gameplay public override IEnumerable Objects => new[] { new HitCircle { HitWindows = HitWindows } }; - public override event Action NewResult; - public override event Action RevertResult; + public override event Action NewResult + { + add => throw new InvalidOperationException(); + remove => throw new InvalidOperationException(); + } + + public override event Action RevertResult + { + add => throw new InvalidOperationException(); + remove => throw new InvalidOperationException(); + } public override Playfield Playfield { get; } public override Container Overlays { get; } @@ -251,9 +260,6 @@ namespace osu.Game.Tests.Visual.Gameplay public TestDrawableRuleset() : base(new OsuRuleset()) { - // won't compile without this. - NewResult?.Invoke(null); - RevertResult?.Invoke(null); } public override void SetReplayScore(Score replayScore) => throw new NotImplementedException(); diff --git a/osu.Game/Graphics/UserInterface/LoadingButton.cs b/osu.Game/Graphics/UserInterface/LoadingButton.cs index 81dc023d7e..a75cf0639c 100644 --- a/osu.Game/Graphics/UserInterface/LoadingButton.cs +++ b/osu.Game/Graphics/UserInterface/LoadingButton.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -22,15 +24,9 @@ namespace osu.Game.Graphics.UserInterface Enabled.Value = !isLoading; if (value) - { loading.Show(); - OnLoadStarted(); - } else - { loading.Hide(); - OnLoadFinished(); - } } } @@ -44,18 +40,34 @@ namespace osu.Game.Graphics.UserInterface protected LoadingButton() { - AddRange(new[] + Add(loading = new LoadingSpinner { - CreateContent(), - loading = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(12) - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12), + Depth = -1, }); } + [BackgroundDependencyLoader] + private void load() + { + Add(CreateContent()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + loading.State.BindValueChanged(s => + { + if (s.NewValue == Visibility.Visible) + OnLoadStarted(); + else + OnLoadFinished(); + }, true); + } + protected override bool OnClick(ClickEvent e) { if (!Enabled.Value) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index e0632ace58..461a06a634 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -22,6 +23,11 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header) : base(header) + { + } + + [BackgroundDependencyLoader] + private void load() { Current.BindTo(filter.Current); } @@ -31,6 +37,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Creates a filter control that can be used to simultaneously select multiple values of type . /// + [NotNull] protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter(); protected class MultipleSelectionFilter : FillFlowContainer diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 52dea63ab7..49a33ee5d6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Changelog public static LocalisableString ListingString => LayoutStrings.HeaderChangelogIndex; + private readonly Bindable currentStream = new Bindable(); + private Box streamsBackground; public ChangelogHeader() @@ -39,7 +41,7 @@ namespace osu.Game.Overlays.Changelog Build.ValueChanged += showBuild; - Streams.Current.ValueChanged += e => + currentStream.ValueChanged += e => { if (e.NewValue?.LatestBuild != null && !e.NewValue.Equals(Build.Value?.UpdateStream)) Build.Value = e.NewValue.LatestBuild; @@ -67,7 +69,7 @@ namespace osu.Game.Overlays.Changelog else { Current.Value = ListingString; - Streams.Current.Value = null; + currentStream.Value = null; } } @@ -92,7 +94,7 @@ namespace osu.Game.Overlays.Changelog Horizontal = 65, Vertical = 20 }, - Child = Streams = new ChangelogUpdateStreamControl() + Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream }, } } }; @@ -110,7 +112,7 @@ namespace osu.Game.Overlays.Changelog if (Build.Value == null) return; - Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name); + currentStream.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name); } private class ChangelogHeaderTitle : OverlayTitle diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 20a8ab64f7..3ff4cfad4a 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -175,6 +175,8 @@ namespace osu.Game.Overlays.Comments protected override IEnumerable EffectTargets => new[] { background }; + private readonly string text; + [Resolved] private OverlayColourProvider colourProvider { get; set; } @@ -184,10 +186,10 @@ namespace osu.Game.Overlays.Comments public CommitButton(string text) { + this.text = text; + AutoSizeAxes = Axes.Both; LoadingAnimationSize = new Vector2(10); - - drawableText.Text = text; } [BackgroundDependencyLoader] @@ -232,7 +234,8 @@ namespace osu.Game.Overlays.Comments Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Margin = new MarginPadding { Horizontal = 20 } + Margin = new MarginPadding { Horizontal = 20 }, + Text = text, } } }; diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index ca5fc90027..7fe188eb04 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -37,11 +37,7 @@ namespace osu.Game.Overlays Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(20), - Action = () => - { - ScrollToStart(); - Button.State = Visibility.Hidden; - } + Action = scrollToTop }); } @@ -58,6 +54,12 @@ namespace osu.Game.Overlays Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } + private void scrollToTop() + { + ScrollToStart(); + Button.State = Visibility.Hidden; + } + public class ScrollToTopButton : OsuHoverContainer { private const int fade_duration = 500; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 4849d6ea36..60b9c29fe0 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -30,11 +30,6 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; - [Obsolete("Use the mod-supporting override")] // can be removed 20210731 - public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() }; - -#pragma warning disable 618 - public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => CreateReplayScore(beatmap); -#pragma warning restore 618 + public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { Replay = new Replay() }; } }