diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index 18d3d29bdc..2426f8c886 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); - AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); + AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault); } [Test] @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addPlacementSteps(times, positions); addPathCheckStep(times, positions); - AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); + AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index f25b66c360..beba5811fe 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor double[] times = { 100, 300 }; float[] positions = { 200, 300 }; addBlueprintStep(times, positions); - AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); + AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault); addDragStartStep(times[1], positions[1]); AddMouseMoveStep(times[1], 400); - AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); + AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault); } [Test] diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 7774a7da09..2c8ef9eae0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps var xPositionData = obj as IHasXPosition; var yPositionData = obj as IHasYPosition; var comboData = obj as IHasCombo; + var sliderVelocityData = obj as IHasSliderVelocity; switch (obj) { @@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0, - LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y + LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, + SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1 }.Yield(); case IHasDuration endTime: diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 74d6565600..7a577f8a83 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { // The SV setting may need to be changed for the current path. - var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable; + var svBindable = hitObject.SliderVelocityBindable; double svToVelocityFactor = hitObject.Velocity / svBindable.Value; double requiredVelocity = path.ComputeRequiredVelocity(); diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index af03c9acab..4c66c054e1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects public override Judgement CreateJudgement() => new CatchBananaJudgement(); - private static readonly List samples = new List { new BananaHitSampleInfo() }; + private static readonly IList default_banana_samples = new List { new BananaHitSampleInfo() }.AsReadOnly(); public Banana() { - Samples = samples; + Samples = default_banana_samples; } // override any external colour changes with banananana @@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Objects } } - private class BananaHitSampleInfo : HitSampleInfo, IEquatable + public class BananaHitSampleInfo : HitSampleInfo, IEquatable { private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; public override IEnumerable LookupNames => lookup_names; - public BananaHitSampleInfo(int volume = 0) + public BananaHitSampleInfo(int volume = 100) : base(string.Empty, volume: volume) { } diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index b45f95a8e6..5bd4ac86f5 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.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 System.Collections.Generic; using System.Threading; +using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; @@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = time, BananaIndex = i, + Samples = new List { new Banana.BananaHitSampleInfo(GetSampleInfo().Volume) } }); time += spacing; diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 96e2d5c4e5..169e99c90c 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchHitObject, IHasPathWithRepeats + public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity { /// /// Positional distance that results in a duration of one second, before any speed adjustments. @@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } + public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + { + Precision = 0.01, + MinValue = 0.1, + MaxValue = 10 + }; + + public double SliderVelocity + { + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; + } + [JsonIgnore] private double velocityFactor; @@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects private double tickDistanceFactor; [JsonIgnore] - public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity; + public double Velocity => velocityFactor * SliderVelocity; [JsonIgnore] - public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; + public double TickDistance => tickDistanceFactor * SliderVelocity; /// /// The length of one span of this . diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 2bdd0e16ad..91b7be6e8f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy @@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Debug.Assert(distanceData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); - DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint; double beatLength; -#pragma warning disable 618 - if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) -#pragma warning restore 618 - beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; + if (hitObject.LegacyBpmMultiplier.HasValue) + beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value; + else if (hitObject is IHasSliderVelocity hasSliderVelocity) + beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity; else - beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; + beatLength = timingPoint.BeatLength; SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 6e1c6cf80f..372ef1e164 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -350,13 +350,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. - if (HitObject.SampleControlPoint == null) - { - throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." - + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - } - - slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + slidingSample.Samples = HitObject.CreateSlidingSamples().Cast().ToArray(); } public override void StopAllSamples() diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json index 6f1d45ad8c..4d298bb671 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json @@ -10,7 +10,7 @@ ["Gameplay/soft-hitnormal"], ["Gameplay/drum-hitnormal"] ], - "Samples": ["Gameplay/-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }, { "StartTime": 1875.0, "EndTime": 2750.0, @@ -19,7 +19,7 @@ ["Gameplay/soft-hitnormal"], ["Gameplay/drum-hitnormal"] ], - "Samples": ["Gameplay/-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }] }, { "StartTime": 3750.0, diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index e7ac38c20e..b05c755bfd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) - && mergedSlider.Samples.SequenceEqual(slider1.Samples) - && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); + && mergedSlider.Samples.SequenceEqual(slider1.Samples); }); AddAssert("slider end is at same completion for last slider", () => diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 6cb77c7b92..a104433ea9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { if (slider is null) return; - slider.SampleControlPoint.SampleBank = "soft"; - slider.SampleControlPoint.SampleVolume = 70; - sample = new HitSampleInfo("hitwhistle"); - slider.Samples.Add(sample); + sample = new HitSampleInfo("hitwhistle", "soft", volume: 70); + slider.Samples.Add(sample.With()); }); AddStep("select added slider", () => @@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("sliders have hitsounds", hasHitsounds); bool hasHitsounds() => sample is not null && - EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && - o.SampleControlPoint.SampleVolume == 70 && - o.Samples.Contains(sample)); + EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample)); } private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs index 53465d43c9..a162d9a491 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs @@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Precision.AlmostEquals(circle.StartTime, time, 1) && Precision.AlmostEquals(circle.Position, position, 0.01f) && circle.NewCombo == startsNewCombo - && circle.Samples.SequenceEqual(slider.HeadCircle.Samples) - && circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint); + && circle.Samples.SequenceEqual(slider.HeadCircle.Samples); } private bool sliderRestored(Slider slider) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs new file mode 100644 index 0000000000..e72a1f79f5 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModBubbles : OsuModTestScene + { + [Test] + public void TestOsuModBubbles() => CreateModTest(new ModTestData + { + Mod = new OsuModBubbles(), + Autoplay = true, + PassCondition = () => true + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png new file mode 100644 index 0000000000..258162c486 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 907422858e..c84a6ab70f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -18,6 +19,7 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable background; + private readonly Bindable ripples = new Bindable(); + public TestSceneGameplayCursor() { var ruleset = new OsuRuleset(); @@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests }); }); + AddToggleStep("ripples", v => ripples.Value = v); + AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); @@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("test cursor container", () => loadContent(false)); } + [BackgroundDependencyLoader] + private void load() + { + var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); + rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples); + } + [TestCase(1, 1)] [TestCase(5, 1)] [TestCase(10, 1)] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 5d9316a21b..ee70441688 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { - DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + SliderVelocity = 0.1f; DefaultsApplied += _ => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 1e9f931b74..4ad78a3190 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -15,6 +15,10 @@ using osuTK.Graphics; using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -22,6 +26,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Configuration; namespace osu.Game.Rulesets.Osu.Tests { @@ -30,6 +35,27 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; + private readonly BindableBool snakingIn = new BindableBool(); + private readonly BindableBool snakingOut = new BindableBool(); + + [SetUpSteps] + public void SetUpSteps() + { + AddToggleStep("toggle snaking", v => + { + snakingIn.Value = v; + snakingOut.Value = v; + }); + } + + [BackgroundDependencyLoader] + private void load() + { + var config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); + config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn); + config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); + } + [Test] public void TestVariousSliders() { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index a32f0a13b8..fc2e6d1f72 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider_start, Position = new Vector2(0, 0), - DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity }, + SliderVelocity = velocity, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 5f27cdc191..d83926ab9b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -8,7 +8,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider_start, Position = new Vector2(0, 0), - DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }, + SliderVelocity = 0.1f, Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 29e6fc4301..f4257a9ee7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { - DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + SliderVelocity = 0.1f; DefaultsApplied += _ => { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index e9518895be..d03ee81f0d 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { var positionData = original as IHasPosition; var comboData = original as IHasCombo; + var sliderVelocityData = original as IHasSliderVelocity; + var generateTicksData = original as IHasGenerateTicks; switch (original) { @@ -47,7 +49,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in h is Slider && h.GetEndTime() < HitObject.StartTime)? - .DifficultyControlPoint?.DeepClone() as DifficultyControlPoint; + double? nearestSliderVelocity = (editorBeatmap.HitObjects + .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity; - HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); + HitObject.SliderVelocity = nearestSliderVelocity ?? 1; HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index e444287b73..6685507ee0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders var splitControlPoints = controlPoints.Take(index + 1).ToList(); controlPoints.RemoveRange(0, index); - // Turn the control points which were split off into a new slider. - var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); - var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone(); - var newSlider = new Slider { StartTime = HitObject.StartTime, Position = HitObject.Position + splitControlPoints[0].Position, NewCombo = HitObject.NewCombo, - SampleControlPoint = samplePoint, - DifficultyControlPoint = difficultyPoint, LegacyLastTickOffset = HitObject.LegacyLastTickOffset, Samples = HitObject.Samples.Select(s => s.With()).ToList(), RepeatCount = HitObject.RepeatCount, @@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); - var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); - samplePoint.Time = time; - editorBeatmap.Add(new HitCircle { StartTime = time, Position = position, NewCombo = i == 0 && HitObject.NewCombo, - SampleControlPoint = samplePoint, Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList() }); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7a70257f3a..ff1e208186 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit private void load() { // Give a bit of breathing room around the playfield content. - PlayfieldContentContainer.Padding = new MarginPadding(10); + PlayfieldContentContainer.Padding = new MarginPadding + { + Vertical = 10, + Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, + Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10, + }; LayerBelowRuleset.AddRange(new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 6d5280e528..2a6d6ce4c3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit StartTime = firstHitObject.StartTime, Position = firstHitObject.Position, NewCombo = firstHitObject.NewCombo, - SampleControlPoint = firstHitObject.SampleControlPoint, Samples = firstHitObject.Samples, }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs index 9e71f657ce..2394cf92fc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs @@ -1,6 +1,7 @@ // 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.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObject { + public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) }; + public void ApplyToDrawableHitObject(DrawableHitObject d) { d.OnUpdate += _ => diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs new file mode 100644 index 0000000000..12e2090f89 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs @@ -0,0 +1,214 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject, IApplicableToScoreProcessor + { + public override string Name => "Bubbles"; + + public override string Acronym => "BU"; + + public override LocalisableString Description => "Don't let their popping distract you!"; + + public override double ScoreMultiplier => 1; + + public override ModType Type => ModType.Fun; + + // Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect + public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + + private PlayfieldAdjustmentContainer bubbleContainer = null!; + + private readonly Bindable currentCombo = new BindableInt(); + + private float maxSize; + private float bubbleSize; + private double bubbleFade; + + private readonly DrawablePool bubblePool = new DrawablePool(100); + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + currentCombo.BindTo(scoreProcessor.Combo); + currentCombo.BindValueChanged(combo => + maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true); + } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + // Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen + // Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size + bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType().First().Radius * 1.90f); + bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType().First().TimePreempt * 2; + + // We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering) + drawableRuleset.Playfield.DisplayJudgements.Value = false; + + bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer(); + + drawableRuleset.Overlays.Add(bubbleContainer); + } + + public void ApplyToDrawableHitObject(DrawableHitObject drawableObject) + { + drawableObject.OnNewResult += (drawable, _) => + { + if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return; + + switch (drawableOsuHitObject.HitObject) + { + case Slider: + case SpinnerTick: + break; + + default: + addBubble(); + break; + } + + void addBubble() + { + BubbleDrawable bubble = bubblePool.Get(); + + bubble.DrawableOsuHitObject = drawableOsuHitObject; + bubble.InitialSize = new Vector2(bubbleSize); + bubble.FadeTime = bubbleFade; + bubble.MaxSize = maxSize; + + bubbleContainer.Add(bubble); + } + }; + + drawableObject.OnRevertResult += (drawable, _) => + { + if (drawable.HitObject is SpinnerTick or Slider) return; + + BubbleDrawable? lastBubble = bubbleContainer.OfType().LastOrDefault(); + + lastBubble?.ClearTransforms(); + lastBubble?.Expire(true); + }; + } + + #region Pooled Bubble drawable + + private partial class BubbleDrawable : PoolableDrawable + { + public DrawableOsuHitObject? DrawableOsuHitObject { get; set; } + + public Vector2 InitialSize { get; set; } + + public float MaxSize { get; set; } + + public double FadeTime { get; set; } + + private readonly Box colourBox; + private readonly CircularContainer content; + + public BubbleDrawable() + { + Origin = Anchor.Centre; + InternalChild = content = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + MaskingSmoothness = 2, + BorderThickness = 0, + BorderColour = Colour4.White, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 3, + Colour = Colour4.Black.Opacity(0.05f), + }, + Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, } + }; + } + + protected override void PrepareForUse() + { + Debug.Assert(DrawableOsuHitObject.IsNotNull()); + + Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black; + Scale = new Vector2(1); + Position = getPosition(DrawableOsuHitObject); + Size = InitialSize; + + //We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect. + ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f); + + // The absolute length of the bubble's animation, can be used in fractions for animations of partial length + double duration = 1700 + Math.Pow(FadeTime, 1.07f); + + // Main bubble scaling based on combo + this.FadeTo(1) + .ScaleTo(MaxSize, duration * 0.8f) + .Then() + // Pop at the end of the bubbles life time + .ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint) + .FadeOut(duration * 0.2f, Easing.OutCirc).Expire(); + + if (!DrawableOsuHitObject.IsHit) return; + + content.BorderThickness = InitialSize.X / 3.5f; + content.BorderColour = Colour4.White; + + colourBox.FadeColour(colourDarker); + + content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint); + // Ripple effect utilises the border to reduce drawable count + content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint) + .Then() + // Avoids transparency overlap issues during the bubble "pop" + .TransformTo(nameof(BorderThickness), 0f); + } + + private Vector2 getPosition(DrawableOsuHitObject drawableObject) + { + switch (drawableObject) + { + // SliderHeads are derived from HitCircles, + // so we must handle them before to avoid them using the wrong positioning logic + case DrawableSliderHead: + return drawableObject.HitObject.Position; + + // Using hitobject position will cause issues with HitCircle placement due to stack leniency. + case DrawableHitCircle: + return drawableObject.Position; + + default: + return drawableObject.HitObject.Position; + } + } + } + + #endregion + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 38d90eb121..c8c4cd6a14 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!"; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) }; [SettingSource("Attraction strength", "How strong the pull is.", 0)] public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 31a6b69d6b..28d459cedb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Hit objects run away!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) }; [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index a7b02596d5..664a8146e7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. - if (HitObject.SampleControlPoint == null) - { - throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." - + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - } - - Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); - slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + Samples.Samples = HitObject.TailSamples.Cast().ToArray(); + slidingSample.Samples = HitObject.CreateSlidingSamples().Cast().ToArray(); } public override void StopAllSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 3446d41fb4..fc4863f164 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,12 +87,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateInitialTransforms() { + // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. + bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0; + animDuration = Math.Min(300, HitObject.SpanDuration); - this.Animate( - d => d.FadeIn(animDuration), - d => d.ScaleTo(0.5f).ScaleTo(1f, animDuration * 2, Easing.OutElasticHalf) - ); + this + .FadeOut() + .Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0) + .FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 2c1b68e05a..d9501f7d58 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -91,7 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateInitialTransforms(); - CirclePiece.FadeInFromZero(HitObject.TimeFadeIn); + // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. + bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0; + + CirclePiece + .FadeOut() + .Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0) + .FadeIn(HitObject.TimeFadeIn); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a5193f1b6e..0ceda1d4b0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + spinningSample.Samples = HitObject.CreateSpinningSamples().Cast().ToArray(); spinningSample.Frequency.Value = spinning_sample_initial_frequency; } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 6c2be8a49a..4189f8ba1e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects; using System.Linq; using System.Threading; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Slider : OsuHitObject, IHasPathWithRepeats + public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; @@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects /// public bool OnlyJudgeNestedObjects = true; + public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + { + Precision = 0.01, + MinValue = 0.1, + MaxValue = 10 + }; + + public double SliderVelocity + { + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; + } + + public bool GenerateTicks { get; set; } = true; + [JsonIgnore] public SliderHeadCircle HeadCircle { get; protected set; } @@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); -#pragma warning disable 618 - var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; -#pragma warning restore 618 - double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; - bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true; + double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; + TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index 35bec92354..f52c3ab382 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -39,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects } else { - // taken from osu-stable - const float first_end_circle_preempt_adjust = 2 / 3f; - // The first end circle should fade in with the slider. - TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust; + TimePreempt += StartTime - slider.StartTime; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index ed6f8a9a6a..df5898fd67 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects AddNested(i < SpinsRequired ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration } - : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration }); + : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { GetSampleInfo("spinnerbonus") } }); } } @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects return new[] { - SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin") + referenceSample.With("spinnerspin") }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 81cdf5755b..00ceccaf7b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -3,7 +3,6 @@ #nullable disable -using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerBonusTick : SpinnerTick { - public SpinnerBonusTick() - { - Samples.Add(new HitSampleInfo("spinnerbonus")); - } - public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 48056a49de..922594a93a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu new OsuModNoScope(), new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new ModAdaptiveSpeed(), - new OsuModFreezeFrame() + new OsuModFreezeFrame(), + new OsuModBubbles() }; case ModType.System: diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 8fdf3821fa..52fdfea95f 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu Cursor, CursorTrail, CursorParticles, + CursorRipple, SliderScorePoint, ReverseArrow, HitCircleText, diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs index d6ce793c7e..461b4a3b45 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs @@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) { - this.FadeOut(duration, Easing.OutQuint); + // intentionally pile on an extra FadeOut to make it happen much faster + this.FadeOut(duration / 4, Easing.OutQuint); icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index fbe094ef81..e6166e9441 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -3,11 +3,13 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { @@ -18,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable proxy = null!; + private Bindable accentColour = null!; + + private bool textureIsDefaultSkin; + + private Drawable arrow = null!; + [BackgroundDependencyLoader] private void load(ISkinSource skinSource) { @@ -26,7 +34,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); - InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty(); + + InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty()); + textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; } protected override void LoadComplete() @@ -39,6 +49,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { drawableHitObject.HitObjectApplied += onHitObjectApplied; onHitObjectApplied(drawableHitObject); + + accentColour = drawableHitObject.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(c => + { + arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White; + }, true); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 620540b8ef..f049aa088f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case OsuSkinComponents.CursorRipple: + if (GetTexture("cursor-ripple") != null) + { + var ripple = this.GetAnimation("cursor-ripple", false, false); + + // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible. + // If anyone complains about these not being applied, this can be uncommented. + // + // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size, + // so we might be okay. + // + // if (ripple != null) + // { + // ripple.Scale = new Vector2(0.5f); + // ripple.Alpha = 0.2f; + // } + + return ripple; + } + + return null; + case OsuSkinComponents.CursorParticles: if (GetTexture("star2") != null) return new LegacyCursorParticles(); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs new file mode 100644 index 0000000000..076d97d06a --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -0,0 +1,105 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI.Cursor +{ + public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler + { + private readonly Bindable showRipples = new Bindable(true); + + private readonly DrawablePool ripplePool = new DrawablePool(20); + + public CursorRippleVisualiser() + { + RelativeSizeAxes = Axes.Both; + } + + public Vector2 CursorScale { get; set; } = Vector2.One; + + [BackgroundDependencyLoader(true)] + private void load(OsuRulesetConfigManager? rulesetConfig) + { + rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (showRipples.Value) + { + AddInternal(ripplePool.Get(r => + { + r.Position = e.MousePosition; + r.Scale = CursorScale; + })); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private partial class CursorRipple : PoolableDrawable + { + private Drawable ripple = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple()) + { + Blending = BlendingParameters.Additive, + }; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + ClearTransforms(true); + + ripple.ScaleTo(0.1f) + .ScaleTo(1, 700, Easing.Out); + + this + .FadeOutFromOne(700) + .Expire(true); + } + } + + public partial class DefaultCursorRipple : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new RingPiece(3) + { + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2), + Alpha = 0.1f, + } + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 5d7648b073..bf1ff872dd 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private Bindable userCursorScale; private Bindable autoCursorScale; + private readonly CursorRippleVisualiser rippleVisualiser; + public OsuCursorContainer() { InternalChild = fadeContainer = new Container @@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Children = new[] { cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling), + rippleVisualiser = new CursorRippleVisualiser(), new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling), } }; @@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor var newScale = new Vector2(e.NewValue); ActiveCursor.Scale = newScale; + rippleVisualiser.CursorScale = newScale; cursorTrail.Scale = newScale; }, true); diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 64c4e7eef6..0e410dbf57 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI LabelText = RulesetSettingsStrings.CursorTrail, Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.CursorRipples, + Current = config.GetBindable(OsuRulesetSetting.ShowCursorRipples) + }, new SettingsEnumDropdown { LabelText = RulesetSettingsStrings.PlayfieldBorderStyle, diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 6a35e9376b..e298e313df 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps foreach (HitObject hitObject in original.HitObjects) { - double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity; + if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue; + + double nextScrollSpeed = hasSliderVelocity.SliderVelocity; EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime); if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision)) @@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = taikoDuration, - TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4 + TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4, + SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 }; } @@ -177,15 +180,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); - DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint; double beatLength; -#pragma warning disable 618 - if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) -#pragma warning restore 618 - beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; + if (obj.LegacyBpmMultiplier.HasValue) + beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value; + else if (obj is IHasSliderVelocity hasSliderVelocity) + beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity; else - beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; + beatLength = timingPoint.BeatLength; double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 3325eda7cf..b4a12fd314 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,8 +3,11 @@ #nullable disable +using System.Linq; using osu.Game.Rulesets.Objects.Types; using System.Threading; +using osu.Framework.Bindables; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -15,7 +18,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoStrongableHitObject, IHasPath + public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double Velocity { get; private set; } + public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + { + Precision = 0.01, + MinValue = 0.1, + MaxValue = 10 + }; + + public double SliderVelocity + { + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; + } + /// /// Numer of ticks per beat length. /// @@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; + double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; tickSpacing = timingPoint.BeatLength / TickRate; @@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects FirstTick = first, TickSpacing = tickSpacing, StartTime = t, - IsStrong = IsStrong + IsStrong = IsStrong, + Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList() }); first = false; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index d4d59d5d44..0043f231d2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects if (IsStrongBindable.Value != strongSamples.Any()) { if (IsStrongBindable.Value) - Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + Samples.Add(GetSampleInfo(HitSampleInfo.HIT_FINISH)); else { foreach (var sample in strongSamples) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 4809791af8..92f2b74568 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI public void Play(HitType hitType) { - var hitObject = GetMostValidObject(); + var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (hitObject == null) + if (hitSample == null) return; - PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) }); + PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) }); } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index d898650b66..2c4f193327 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -510,7 +510,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); } - static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); + static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; } [Test] @@ -528,7 +528,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); } - static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); + static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; } [Test] @@ -548,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); } - static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); + static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; } [Test] diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs index 1e1c214c30..5a3ef619d1 100644 --- a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs @@ -37,45 +37,6 @@ namespace osu.Game.Tests.Editing.Checks cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); } - [Test] - public void TestNormalControlPointVolume() - { - var hitCircle = new HitCircle - { - StartTime = 0, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty()); - - assertOk(new List { hitCircle }); - } - - [Test] - public void TestLowControlPointVolume() - { - var hitCircle = new HitCircle - { - StartTime = 1000, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty()); - - assertLowVolume(new List { hitCircle }); - } - - [Test] - public void TestMutedControlPointVolume() - { - var hitCircle = new HitCircle - { - StartTime = 2000, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty()); - - assertMuted(new List { hitCircle }); - } - [Test] public void TestNormalSampleVolume() { @@ -122,7 +83,7 @@ namespace osu.Game.Tests.Editing.Checks var sliderHead = new SliderHeadCircle { StartTime = 0, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } }; sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); @@ -135,7 +96,7 @@ namespace osu.Game.Tests.Editing.Checks var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) { - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } }; slider.ApplyDefaults(cpi, new BeatmapDifficulty()); @@ -155,13 +116,13 @@ namespace osu.Game.Tests.Editing.Checks var sliderTick = new SliderTick { StartTime = 250, - Samples = new List { new HitSampleInfo("slidertick") } + Samples = new List { new HitSampleInfo("slidertick", volume: volume_regular) } }; sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) { - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail. + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } // Applies to the tail. }; slider.ApplyDefaults(cpi, new BeatmapDifficulty()); @@ -174,14 +135,14 @@ namespace osu.Game.Tests.Editing.Checks var sliderHead = new SliderHeadCircle { StartTime = 0, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } }; sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); var sliderTick = new SliderTick { StartTime = 250, - Samples = new List { new HitSampleInfo("slidertick") } + Samples = new List { new HitSampleInfo("slidertick", volume: volume_regular) } }; sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); @@ -194,59 +155,6 @@ namespace osu.Game.Tests.Editing.Checks assertMutedPassive(new List { slider }); } - [Test] - public void TestMutedControlPointVolumeSliderHead() - { - var sliderHead = new SliderHeadCircle - { - StartTime = 2000, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); - - var sliderTick = new SliderTick - { - StartTime = 2250, - Samples = new List { new HitSampleInfo("slidertick") } - }; - sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); - - var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500) - { - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } - }; - slider.ApplyDefaults(cpi, new BeatmapDifficulty()); - - assertMuted(new List { slider }); - } - - [Test] - public void TestMutedControlPointVolumeSliderTail() - { - var sliderHead = new SliderHeadCircle - { - StartTime = 0, - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); - - var sliderTick = new SliderTick - { - StartTime = 250, - Samples = new List { new HitSampleInfo("slidertick") } - }; - sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); - - // Ends after the 5% control point. - var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500) - { - Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } - }; - slider.ApplyDefaults(cpi, new BeatmapDifficulty()); - - assertMutedPassive(new List { slider }); - } - private void assertOk(List hitObjects) { Assert.That(check.Run(getContext(hitObjects)), Is.Empty); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index f556f6e2fe..6399507aa0 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; @@ -74,12 +75,9 @@ namespace osu.Game.Tests.Editing [TestCase(2)] public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) { - assertSnapDistance(100, new HitObject + assertSnapDistance(100, new Slider { - DifficultyControlPoint = new DifficultyControlPoint - { - SliderVelocity = multiplier - } + SliderVelocity = multiplier }, false); } @@ -87,12 +85,9 @@ namespace osu.Game.Tests.Editing [TestCase(2)] public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier) { - assertSnapDistance(100 * multiplier, new HitObject + assertSnapDistance(100 * multiplier, new Slider { - DifficultyControlPoint = new DifficultyControlPoint - { - SliderVelocity = multiplier - } + SliderVelocity = multiplier }, true); } @@ -114,12 +109,9 @@ namespace osu.Game.Tests.Editing const float base_distance = 100; const float slider_velocity = 1.2f; - var referenceObject = new HitObject + var referenceObject = new Slider { - DifficultyControlPoint = new DifficultyControlPoint - { - SliderVelocity = slider_velocity - } + SliderVelocity = slider_velocity }; assertSnapDistance(base_distance * slider_velocity, referenceObject, true); diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs index c70ad751be..99b85d0502 100644 --- a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs +++ b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs @@ -1,11 +1,14 @@ -#include "sh_Utils.h" +#define HIGH_PRECISION_VERTEX -varying mediump vec2 v_TexCoord; -varying mediump vec4 v_TexRect; +#include "sh_Utils.h" +#include "sh_Masking.h" + +layout(location = 2) in highp vec2 v_TexCoord; + +layout(location = 0) out vec4 o_Colour; void main(void) { - float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]); - gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1)); + highp float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]); + o_Colour = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord); } - diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs index 4485356fa4..505554bb33 100644 --- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs +++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs @@ -1,31 +1,25 @@ -#include "sh_Utils.h" +layout(location = 0) in highp vec2 m_Position; +layout(location = 1) in lowp vec4 m_Colour; +layout(location = 2) in highp vec2 m_TexCoord; +layout(location = 3) in highp vec4 m_TexRect; +layout(location = 4) in mediump vec2 m_BlendRange; -attribute highp vec2 m_Position; -attribute lowp vec4 m_Colour; -attribute mediump vec2 m_TexCoord; -attribute mediump vec4 m_TexRect; -attribute mediump vec2 m_BlendRange; - -varying highp vec2 v_MaskingPosition; -varying lowp vec4 v_Colour; -varying mediump vec2 v_TexCoord; -varying mediump vec4 v_TexRect; -varying mediump vec2 v_BlendRange; - -uniform highp mat4 g_ProjMatrix; -uniform highp mat3 g_ToMaskingSpace; +layout(location = 0) out highp vec2 v_MaskingPosition; +layout(location = 1) out lowp vec4 v_Colour; +layout(location = 2) out highp vec2 v_TexCoord; +layout(location = 3) out highp vec4 v_TexRect; +layout(location = 4) out mediump vec2 v_BlendRange; void main(void) { - // Transform from screen space to masking space. - highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); - v_MaskingPosition = maskingPos.xy / maskingPos.z; + // Transform from screen space to masking space. + highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); + v_MaskingPosition = maskingPos.xy / maskingPos.z; - v_Colour = m_Colour; - v_TexCoord = m_TexCoord; - v_TexRect = m_TexRect; - v_BlendRange = m_BlendRange; + v_Colour = m_Colour; + v_TexCoord = m_TexCoord; + v_TexRect = m_TexRect; + v_BlendRange = m_BlendRange; - gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0); + gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); } - diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index 585a3f95e7..f0a9ce7beb 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -53,6 +53,8 @@ namespace osu.Game.Tests.Testing { Dependencies.Get().GetRawData(@"sh_TestVertex.vs"); Dependencies.Get().GetRawData(@"sh_TestFragment.fs"); + Dependencies.Get().Load(@"TestVertex", @"TestFragment"); + Dependencies.Get().Load(VertexShaderDescriptor.TEXTURE_2, @"TestFragment"); }); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index d26bb6bb8a..c4c05278b5 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; @@ -95,10 +94,6 @@ namespace osu.Game.Tests.Visual.Editing var path = slider.Path; return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints); }); - - // see `HitObject.control_point_leniency`. - AddAssert("sample control point has correct time", () => Precision.AlmostEquals(slider.SampleControlPoint.Time, slider.GetEndTime(), 1)); - AddAssert("difficulty control point has correct time", () => slider.DifficultyControlPoint.Time == slider.StartTime); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index b396b382ff..64c48e74cf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -122,19 +122,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); - // After placement these must be non-default as defaults are read-only. - AddAssert("Placed object has non-default control points", () => - !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) && - !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT)); - ReloadEditorToSameBeatmap(); AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); - - // After placement these must be non-default as defaults are read-only. - AddAssert("Placed object still has non-default control points", () => - !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) && - !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT)); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index 7ab0188114..9bdb9a513c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -108,12 +108,16 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + ExpandingToolboxContainer toolboxContainer = null!; + + AddStep("move mouse to toolbox", () => InputManager.MoveMouseTo(toolboxContainer = hitObjectComposer.ChildrenOfType().First())); + AddUntilStep("toolbox is expanded", () => toolboxContainer.Expanded.Value); + AddUntilStep("wait for toolbox to expand", () => toolboxContainer.LatestTransformEndTime, () => Is.EqualTo(Time.Current)); + AddStep("move mouse to overlapping toggle button", () => { var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; - var button = hitObjectComposer - .ChildrenOfType().First() - .ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre)); + var button = toolboxContainer.ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre)); InputManager.MoveMouseTo(button); }); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs index ab82678eb9..3b998b4219 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs @@ -8,10 +8,10 @@ using Humanizer; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -61,10 +61,7 @@ namespace osu.Game.Tests.Visual.Editing new PathControlPoint(new Vector2(100, 0)) } }, - DifficultyControlPoint = new DifficultyControlPoint - { - SliderVelocity = 2 - } + SliderVelocity = 2 }); }); } @@ -100,8 +97,8 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("unify slider velocity", () => { - foreach (var h in EditorBeatmap.HitObjects) - h.DifficultyControlPoint.SliderVelocity = 1.5; + foreach (var h in EditorBeatmap.HitObjects.OfType()) + h.SliderVelocity = 1.5; }); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); @@ -185,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () => { var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); - return h.DifficultyControlPoint.SliderVelocity == velocity; + return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity; }); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs index e8dcc6f19b..7a0418cfec 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs @@ -4,11 +4,12 @@ #nullable disable using System.Linq; +using System.Collections.Generic; using Humanizer; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets; @@ -39,10 +40,9 @@ namespace osu.Game.Tests.Visual.Editing { StartTime = 0, Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2, - SampleControlPoint = new SampleControlPoint + Samples = new List { - SampleBank = "normal", - SampleVolume = 80 + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 80) } }); @@ -50,10 +50,9 @@ namespace osu.Game.Tests.Visual.Editing { StartTime = 500, Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, - SampleControlPoint = new SampleControlPoint + Samples = new List { - SampleBank = "soft", - SampleVolume = 60 + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60) } }); }); @@ -96,7 +95,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("unify sample volume", () => { foreach (var h in EditorBeatmap.HitObjects) - h.SampleControlPoint.SampleVolume = 50; + { + for (int i = 0; i < h.Samples.Count; i++) + { + h.Samples[i] = h.Samples[i].With(newVolume: 50); + } + } }); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); @@ -136,7 +140,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("unify sample bank", () => { foreach (var h in EditorBeatmap.HitObjects) - h.SampleControlPoint.SampleBank = "soft"; + { + for (int i = 0; i < h.Samples.Count; i++) + { + h.Samples[i] = h.Samples[i].With(newBank: "soft"); + } + } }); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); @@ -248,7 +257,7 @@ namespace osu.Game.Tests.Visual.Editing private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () => { var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); - return h.SampleControlPoint.SampleVolume == volume; + return h.Samples.All(o => o.Volume == volume); }); private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => @@ -265,7 +274,7 @@ namespace osu.Game.Tests.Visual.Editing private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () => { var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); - return h.SampleControlPoint.SampleBank == bank; + return h.Samples.All(o => o.Bank == bank); }); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs new file mode 100644 index 0000000000..7f9a69833c --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Game.Database; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene + { + public override void SetUpSteps() + { + CreateInitialBeatmap = () => + { + var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely(); + return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First()); + }; + + base.SetUpSteps(); + } + + [Test] + public void TestLocallyModifyingOnlineBeatmap() + { + AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); + + AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); + SaveEditor(); + + ReloadEditorToSameBeatmap(); + AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 709d796e97..08e036248b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -77,5 +77,39 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType().Single().Duration > 0); } + + [Test] + public void TestDisallowRepeatsOnZeroDurationObjects() + { + DragArea dragArea; + + AddStep("add zero length slider", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 2700 + }); + }); + + AddStep("hold down drag bar", () => + { + // distinguishes between the actual drag bar and its "underlay shadow". + dragArea = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput); + InputManager.MoveMouseTo(dragArea); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("try to extend drag bar", () => + { + var blueprint = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft + new Vector2(100, 0)); + }); + + AddStep("release button", () => InputManager.PressButton(MouseButton.Left)); + + AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType().Single().RepeatCount == 0); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 31133f00d9..114c554d28 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -73,8 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay new HitCircle { StartTime = t += spacing, - Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, - SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, + Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") }, }, new HitCircle { @@ -84,8 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay { StartTime = t += spacing, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), - Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }, - SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, + Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") }, }, }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index c0b6a0beab..9b130071cc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -98,6 +98,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0); + + // A previous test's mod overlay could still be fading out. + AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1); + assertHasFreeModButton(allowedMod, false); assertHasFreeModButton(requiredMod, false); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5ccaebd721..f99fe1d8d4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -14,6 +14,7 @@ using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; @@ -67,6 +68,19 @@ namespace osu.Game.Tests.Visual.UserInterface } } }); + r.Add(new ModPreset + { + Name = "Half Time 0.5x", + Description = "Very slow", + Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Mods = new[] + { + new OsuModHalfTime + { + SpeedChange = { Value = 0.5 } + } + } + }); }); }); } @@ -566,6 +580,28 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("5 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 5); } + [Test] + public void TestModMultiplierUpdates() + { + createScreen(); + + AddStep("select mod preset with half time", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x")); + InputManager.Click(MouseButton.Left); + }); + AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.5)); + + // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, + // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. + AddStep("force collection", GC.Collect); + + AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); + AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() + .ChildrenOfType>().Single().TriggerClick()); + AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6af6a25579..ae62564b0d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -415,6 +415,13 @@ namespace osu.Game.Beatmaps // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. beatmapContent.BeatmapInfo = beatmapInfo; + // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. + // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, + // which influences the beatmap checksums. + beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; + beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + beatmapInfo.ResetOnlineInfo(); + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -438,9 +445,6 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; - beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); updateHashAndMarkDirty(setInfo); diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index c454439c5c..6cd4d74a31 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.ControlPoints public readonly Bindable SampleBankBindable = new Bindable(DEFAULT_BANK) { Default = DEFAULT_BANK }; /// - /// The speed multiplier at this control point. + /// The default sample bank at this control point. /// public string SampleBank { @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps.ControlPoints } /// - /// The default sample bank at this control point. + /// The default sample volume at this control point. /// public readonly BindableInt SampleVolumeBindable = new BindableInt(100) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index ef1dbc0488..5e98025c9a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -3,6 +3,8 @@ #nullable disable +#pragma warning disable 618 + using System; using System.Collections.Generic; using System.IO; @@ -15,7 +17,9 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps.Formats { @@ -26,6 +30,11 @@ namespace osu.Game.Beatmaps.Formats /// public const int EARLY_VERSION_TIMING_OFFSET = 24; + /// + /// A small adjustment to the start time of control points to account for rounding/precision errors. + /// + private const double control_point_leniency = 1; + internal static RulesetStore RulesetStore; private Beatmap beatmap; @@ -85,7 +94,45 @@ namespace osu.Game.Beatmaps.Formats this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); foreach (var hitObject in this.beatmap.HitObjects) - hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); + { + applyDefaults(hitObject); + applySamples(hitObject); + } + } + + private void applyDefaults(HitObject hitObject) + { + DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT; + + if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint) + { + hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier; + if (hitObject is IHasGenerateTicks hasGenerateTicks) + hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks; + } + + if (hitObject is IHasSliderVelocity hasSliderVelocity) + hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity; + + hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + } + + private void applySamples(HitObject hitObject) + { + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT; + + hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); + + if (hitObject is IHasRepeats hasRepeats) + { + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) + { + double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency; + var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT; + + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList(); + } + } } /// @@ -451,9 +498,7 @@ namespace osu.Game.Beatmaps.Formats int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; -#pragma warning disable 618 addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength) -#pragma warning restore 618 { SliderVelocity = speedMultiplier, }, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 072e442dea..7fbcca9adb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -92,7 +92,8 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); - writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}")); + writer.WriteLine(FormattableString.Invariant( + $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); @@ -173,9 +174,6 @@ namespace osu.Game.Beatmaps.Formats private void handleControlPoints(TextWriter writer) { - if (beatmap.ControlPointInfo.Groups.Count == 0) - return; - var legacyControlPoints = new LegacyControlPointInfo(); foreach (var point in beatmap.ControlPointInfo.AllControlPoints) legacyControlPoints.Add(point.Time, point.DeepClone()); @@ -199,33 +197,43 @@ namespace osu.Game.Beatmaps.Formats legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); } + LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties(); + foreach (var group in legacyControlPoints.Groups) { var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault(); + var controlPointProperties = getLegacyControlPointProperties(group, groupTimingPoint != null); // If the group contains a timing control point, it needs to be output separately. if (groupTimingPoint != null) { writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},")); writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},")); - outputControlPointAt(groupTimingPoint.Time, true); + outputControlPointAt(controlPointProperties, true); + lastControlPointProperties = controlPointProperties; + lastControlPointProperties.SliderVelocity = 1; } + if (controlPointProperties.IsRedundant(lastControlPointProperties)) + continue; + // Output any remaining effects as secondary non-timing control point. - var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time); writer.Write(FormattableString.Invariant($"{group.Time},")); - writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},")); - outputControlPointAt(group.Time, false); + writer.Write(FormattableString.Invariant($"{-100 / controlPointProperties.SliderVelocity},")); + outputControlPointAt(controlPointProperties, false); + lastControlPointProperties = controlPointProperties; } - void outputControlPointAt(double time, bool isTimingPoint) + LegacyControlPointProperties getLegacyControlPointProperties(ControlPointGroup group, bool updateSampleBank) { - var samplePoint = legacyControlPoints.SamplePointAt(time); - var effectPoint = legacyControlPoints.EffectPointAt(time); - var timingPoint = legacyControlPoints.TimingPointAt(time); + var timingPoint = legacyControlPoints.TimingPointAt(group.Time); + var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time); + var samplePoint = legacyControlPoints.SamplePointAt(group.Time); + var effectPoint = legacyControlPoints.EffectPointAt(group.Time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); + int customSampleBank = toLegacyCustomSampleBank(tempHitSample); // Convert effect flags to the legacy format LegacyEffectFlags effectFlags = LegacyEffectFlags.None; @@ -234,12 +242,26 @@ namespace osu.Game.Beatmaps.Formats if (timingPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{timingPoint.TimeSignature.Numerator},")); - writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); - writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); - writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); - writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},")); - writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); + return new LegacyControlPointProperties + { + SliderVelocity = difficultyPoint.SliderVelocity, + TimingSignature = timingPoint.TimeSignature.Numerator, + SampleBank = updateSampleBank ? (int)toLegacySampleBank(tempHitSample.Bank) : lastControlPointProperties.SampleBank, + // Inherit the previous custom sample bank if the current custom sample bank is not set + CustomSampleBank = customSampleBank >= 0 ? customSampleBank : lastControlPointProperties.CustomSampleBank, + SampleVolume = tempHitSample.Volume, + EffectFlags = effectFlags + }; + } + + void outputControlPointAt(LegacyControlPointProperties controlPoint, bool isTimingPoint) + { + writer.Write(FormattableString.Invariant($"{controlPoint.TimingSignature.ToString(CultureInfo.InvariantCulture)},")); + writer.Write(FormattableString.Invariant($"{controlPoint.SampleBank.ToString(CultureInfo.InvariantCulture)},")); + writer.Write(FormattableString.Invariant($"{controlPoint.CustomSampleBank.ToString(CultureInfo.InvariantCulture)},")); + writer.Write(FormattableString.Invariant($"{controlPoint.SampleVolume.ToString(CultureInfo.InvariantCulture)},")); + writer.Write(FormattableString.Invariant($"{(isTimingPoint ? "1" : "0")},")); + writer.Write(FormattableString.Invariant($"{((int)controlPoint.EffectFlags).ToString(CultureInfo.InvariantCulture)}")); writer.WriteLine(); } @@ -249,7 +271,10 @@ namespace osu.Game.Beatmaps.Formats yield break; foreach (var hitObject in hitObjects) - yield return hitObject.DifficultyControlPoint; + { + if (hitObject is IHasSliderVelocity hasSliderVelocity) + yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity }; + } } void extractDifficultyControlPoints(IEnumerable hitObjects) @@ -268,7 +293,15 @@ namespace osu.Game.Beatmaps.Formats { foreach (var hitObject in hitObjects) { - yield return hitObject.SampleControlPoint; + if (hitObject.Samples.Count > 0) + { + int volume = hitObject.Samples.Max(o => o.Volume); + int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) + ? hitObject.Samples.OfType().Max(o => o.CustomSampleBank) + : -1; + + yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex }; + } foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) yield return nested; @@ -466,16 +499,16 @@ namespace osu.Game.Beatmaps.Formats if (curveData != null) { - for (int i = 0; i < curveData.NodeSamples.Count; i++) + for (int i = 0; i < curveData.SpanCount() + 1; i++) { - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + writer.Write(FormattableString.Invariant($"{(i < curveData.NodeSamples.Count ? (int)toLegacyHitSoundType(curveData.NodeSamples[i]) : 0)}")); + writer.Write(i != curveData.SpanCount() ? "|" : ","); } - for (int i = 0; i < curveData.NodeSamples.Count; i++) + for (int i = 0; i < curveData.SpanCount() + 1; i++) { - writer.Write(getSampleBank(curveData.NodeSamples[i], true)); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + writer.Write(i < curveData.NodeSamples.Count ? getSampleBank(curveData.NodeSamples[i], true) : "0:0"); + writer.Write(i != curveData.SpanCount() ? "|" : ","); } } } @@ -506,10 +539,18 @@ namespace osu.Game.Beatmaps.Formats if (!banksOnly) { - string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); + int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; + // We want to ignore custom sample banks and volume when not encoding to the mania game mode, + // because they cause unexpected results in the editor and are already satisfied by the control points. + if (onlineRulesetID != 3) + { + customSampleBank = 0; + volume = 0; + } + sb.Append(':'); sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{volume}:")); @@ -562,12 +603,30 @@ namespace osu.Game.Beatmaps.Formats } } - private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) + private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) { if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) - return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture); + return legacy.CustomSampleBank; - return "0"; + return 0; + } + + private struct LegacyControlPointProperties + { + internal double SliderVelocity { get; set; } + internal int TimingSignature { get; init; } + internal int SampleBank { get; init; } + internal int CustomSampleBank { get; init; } + internal int SampleVolume { get; init; } + internal LegacyEffectFlags EffectFlags { get; init; } + + internal bool IsRedundant(LegacyControlPointProperties other) => + SliderVelocity == other.SliderVelocity && + TimingSignature == other.TimingSignature && + SampleBank == other.SampleBank && + CustomSampleBank == other.CustomSampleBank && + SampleVolume == other.SampleVolume && + EffectFlags == other.EffectFlags; } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 70ad6bfc96..1e7d3cf84f 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -157,6 +157,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Scaling, ScalingMode.Off); SetDefault(OsuSetting.SafeAreaConsiderations, true); + SetDefault(OsuSetting.ScalingBackgroundDim, 0.9f, 0.5f, 1f); SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f); SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f); @@ -364,6 +365,7 @@ namespace osu.Game.Configuration ScalingPositionY, ScalingSizeX, ScalingSizeY, + ScalingBackgroundDim, UIScale, IntroSequence, NotifyOnUsernameMentioned, diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index fb5c3e3b60..c47aba2f0c 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -15,6 +15,7 @@ using osu.Game.Configuration; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.Containers { @@ -46,6 +47,8 @@ namespace osu.Game.Graphics.Containers private BackgroundScreenStack backgroundStack; + private Bindable scalingMenuBackgroundDim; + private RectangleF? customRect; private bool customRectIsRelativePosition; @@ -138,6 +141,9 @@ namespace osu.Game.Graphics.Containers safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize)); + + scalingMenuBackgroundDim = config.GetBindable(OsuSetting.ScalingBackgroundDim); + scalingMenuBackgroundDim.ValueChanged += _ => Scheduler.AddOnce(updateSize); } protected override void LoadComplete() @@ -148,7 +154,9 @@ namespace osu.Game.Graphics.Containers sizableContainer.FinishTransforms(); } - private bool requiresBackgroundVisible => (scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays) && (sizeX.Value != 1 || sizeY.Value != 1); + private bool requiresBackgroundVisible => (scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays) + && (sizeX.Value != 1 || sizeY.Value != 1) + && scalingMenuBackgroundDim.Value < 1; private void updateSize() { @@ -161,8 +169,8 @@ namespace osu.Game.Graphics.Containers { AddInternal(backgroundStack = new BackgroundScreenStack { - Colour = OsuColour.Gray(0.1f), Alpha = 0, + Colour = Color4.Black, Depth = float.MaxValue }); @@ -170,6 +178,7 @@ namespace osu.Game.Graphics.Containers } backgroundStack.FadeIn(TRANSITION_DURATION); + backgroundStack.FadeColour(OsuColour.Gray(1.0f - scalingMenuBackgroundDim.Value), TRANSITION_DURATION, Easing.OutQuint); } else backgroundStack?.FadeOut(TRANSITION_DURATION); diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 0c36d73085..0e26029ffa 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -97,7 +97,9 @@ namespace osu.Game.Graphics.UserInterface // Find the number of significant digits (we could have less than 5 after normalize()) int significantDigits = FormatUtils.FindPrecision(decimalPrecision); - return floatValue.ToString($"N{significantDigits}"); + string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; + + return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; } /// diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index fa58ae27f2..c9e1f74917 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -49,11 +49,10 @@ namespace osu.Game.Graphics.UserInterface private const float transition_length = 500; private Sample sampleChecked; private Sample sampleUnchecked; + private readonly SpriteIcon icon; public OsuTabControlCheckbox() { - SpriteIcon icon; - AutoSizeAxes = Axes.Both; Children = new Drawable[] @@ -85,14 +84,6 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.BottomLeft, } }; - - Current.ValueChanged += selected => - { - icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; - text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium); - - updateFade(); - }; } [BackgroundDependencyLoader] @@ -105,6 +96,19 @@ namespace osu.Game.Graphics.UserInterface sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(selected => + { + icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; + text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium); + + updateFade(); + }, true); + } + protected override bool OnHover(HoverEvent e) { updateFade(); diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs index e4058827f3..e6e7ae9305 100644 --- a/osu.Game/Graphics/UserInterface/TimeSlider.cs +++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs @@ -12,6 +12,6 @@ namespace osu.Game.Graphics.UserInterface /// public partial class TimeSlider : RoundedSliderBar { - public override LocalisableString TooltipText => $"{Current.Value:N0} ms"; + public override LocalisableString TooltipText => $"{base.TooltipText} ms"; } } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 1b0df6ecf6..52e6a5eaac 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail"); + /// + /// "Cursor ripples" + /// + public static LocalisableString CursorRipples => new TranslatableString(getKey(@"cursor_ripples"), @"Cursor ripples"); + /// /// "Playfield border style" /// diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 16602db4be..38ae8c68cb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -242,17 +242,21 @@ namespace osu.Game.Overlays.Mods if (AllowCustomisation) ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); - SelectedMods.BindValueChanged(val => + SelectedMods.BindValueChanged(_ => { - modSettingChangeTracker?.Dispose(); - updateMultiplier(); updateFromExternalSelection(); updateCustomisation(); + modSettingChangeTracker?.Dispose(); + if (AllowCustomisation) { - modSettingChangeTracker = new ModSettingChangeTracker(val.NewValue); + // Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can + // potentially be stale, due to complexities in the way change trackers work. + // + // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 + modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); modSettingChangeTracker.SettingChanged += _ => updateMultiplier(); } }, true); diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 00c5ce8002..90fdfd0491 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Music var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); + titlePart.DrawablePartsRecreated += _ => updateSelectionState(SelectedSet.Value, applyImmediately: true); text.AddText(@" "); // to separate the title from the artist. text.AddText(artist, sprite => @@ -66,27 +66,25 @@ namespace osu.Game.Overlays.Music sprite.Padding = new MarginPadding { Top = 1 }; }); - SelectedSet.BindValueChanged(set => - { - bool newSelected = set.NewValue?.Equals(Model) == true; - - if (newSelected == selected) - return; - - selected = newSelected; - updateSelectionState(false); - }); - - updateSelectionState(true); + SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue)); + updateSelectionState(SelectedSet.Value, applyImmediately: true); }); } private bool selected; - private void updateSelectionState(bool instant) + private void updateSelectionState(Live selectedSet, bool applyImmediately = false) { + bool wasSelected = selected; + selected = selectedSet?.Equals(Model) == true; + + // Immediate updates should forcibly set correct state regardless of previous state. + // This ensures that the initial state is correctly applied. + if (wasSelected == selected && !applyImmediately) + return; + foreach (Drawable s in titlePart.Drawables) - s.FadeColour(selected ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION); + s.FadeColour(selected ? colours.Yellow : Color4.White, applyImmediately ? 0 : FADE_DURATION); } protected override Drawable CreateContent() => new DelayedLoadWrapper(text = new OsuTextFlowContainer diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 9752e04f44..2a615f0e12 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -185,6 +185,12 @@ namespace osu.Game.Overlays content.ScaleTo(1, 1000, Easing.OutElastic); base.OnMouseUp(e); } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + return true; + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index a46205d40d..2e26d15105 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader; private FillFlowContainer> scalingSettings = null!; + private SettingsSlider dimSlider = null!; private readonly Bindable currentDisplay = new Bindable(); @@ -57,6 +58,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private Bindable scalingSizeX = null!; private Bindable scalingSizeY = null!; + private Bindable scalingBackgroundDim = null!; + private const int transition_duration = 400; [BackgroundDependencyLoader] @@ -70,6 +73,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); + scalingBackgroundDim = osuConfig.GetBindable(OsuSetting.ScalingBackgroundDim); if (window != null) { @@ -161,6 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics KeyboardStep = 0.01f, DisplayAsPercentage = true }, + dimSlider = new SettingsSlider + { + LabelText = GameplaySettingsStrings.BackgroundDim, + Current = scalingBackgroundDim, + KeyboardStep = 0.01f, + DisplayAsPercentage = true, + }, } }, }; @@ -216,8 +227,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.AutoSizeAxes = scalingMode.Value != ScalingMode.Off ? Axes.Y : Axes.None; scalingSettings.ForEach(s => { - s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything; - s.CanBeShown.Value = scalingMode.Value != ScalingMode.Off; + if (s == dimSlider) + { + s.CanBeShown.Value = scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays; + } + else + { + s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything; + s.CanBeShown.Value = scalingMode.Value != ScalingMode.Off; + } }); } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index 5b59a81f91..a2ae1764dd 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Edit.Checks yield break; // Samples that allow themselves to be overridden by control points have a volume of 0. - int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume); + int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume); double samplePlayTime = sampledHitObject.GetEndTime(); EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime); diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index aa47b4f424..a8972775de 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.TernaryButtons; namespace osu.Game.Rulesets.Edit @@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Edit private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() { - HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject; + HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject; if (lastBefore == null) return null; @@ -239,7 +240,7 @@ namespace osu.Game.Rulesets.Edit public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) { - return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor); } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 653861c11c..e2dbd2acdc 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -116,6 +116,11 @@ namespace osu.Game.Rulesets.Edit PlayfieldContentContainer = new Container { Name = "Content", + Padding = new MarginPadding + { + Left = TOOLBOX_CONTRACTED_SIZE_LEFT, + Right = TOOLBOX_CONTRACTED_SIZE_RIGHT, + }, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Edit Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, - LeftToolbox = new ExpandingToolboxContainer(60, 200) + LeftToolbox = new ExpandingToolboxContainer(TOOLBOX_CONTRACTED_SIZE_LEFT, 200) { Children = new Drawable[] { @@ -173,7 +178,7 @@ namespace osu.Game.Rulesets.Edit Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, - RightToolbox = new ExpandingToolboxContainer(130, 250) + RightToolbox = new ExpandingToolboxContainer(TOOLBOX_CONTRACTED_SIZE_RIGHT, 250) { Child = new EditorToolboxGroup("inspector") { @@ -450,6 +455,9 @@ namespace osu.Game.Rulesets.Edit [Cached] public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { + public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; + public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 130; + public readonly Ruleset Ruleset; protected HitObjectComposer(Ruleset ruleset) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index f810f51027..bdcb334738 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit HitObject = hitObject; // adding the default hit sample should be the case regardless of the ruleset. - HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, volume: 100)); RelativeSizeAxes = Axes.Both; @@ -74,9 +74,10 @@ namespace osu.Game.Rulesets.Edit /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. protected void BeginPlacement(bool commitStart = false) { - var nearestSampleControlPoint = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.SampleControlPoint?.DeepClone() as SampleControlPoint; - - HitObject.SampleControlPoint = nearestSampleControlPoint ?? new SampleControlPoint(); + // Take the hitnormal sample of the last hit object + var lastHitNormal = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + if (lastHitNormal != null) + HitObject.Samples[0] = lastHitNormal; placementHandler.BeginPlacement(HitObject); if (commitStart) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index f8c3a730f2..215fc877dc 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Mods flashlight.RelativeSizeAxes = Axes.Both; flashlight.Colour = Color4.Black; + // Flashlight mods should always draw above any other mod adding overlays. + flashlight.Depth = float.MinValue; flashlight.Combo.BindTo(Combo); + drawableRuleset.Overlays.Add(flashlight); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f6c3452e48..79fc778287 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -357,13 +357,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (samples.Length <= 0) return; - if (HitObject.SampleControlPoint == null) - { - throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." - + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - } - - Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + Samples.Samples = samples.Cast().ToArray(); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 25f538d211..a4cb976d50 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -16,7 +16,6 @@ using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -77,8 +76,11 @@ namespace osu.Game.Rulesets.Objects /// public virtual IList AuxiliarySamples => ImmutableList.Empty; - public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT; - public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT; + /// + /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. + /// DO NOT USE THIS UNLESS 100% SURE. + /// + public double? LegacyBpmMultiplier { get; set; } /// /// Whether this is in Kiai time. @@ -105,25 +107,8 @@ namespace osu.Game.Rulesets.Objects /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { - var legacyInfo = controlPointInfo as LegacyControlPointInfo; - - if (legacyInfo != null) - DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone(); - else if (ReferenceEquals(DifficultyControlPoint, DifficultyControlPoint.DEFAULT)) - DifficultyControlPoint = new DifficultyControlPoint(); - - DifficultyControlPoint.Time = StartTime; - ApplyDefaultsToSelf(controlPointInfo, difficulty); - // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time. - if (legacyInfo != null) - SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone(); - else if (ReferenceEquals(SampleControlPoint, SampleControlPoint.DEFAULT)) - SampleControlPoint = new SampleControlPoint(); - - SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; - nestedHitObjects.Clear(); CreateNestedHitObjects(cancellationToken); @@ -164,9 +149,6 @@ namespace osu.Game.Rulesets.Objects foreach (var nested in nestedHitObjects) nested.StartTime += offset; - - DifficultyControlPoint.Time = time.NewValue; - SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; } } @@ -222,6 +204,17 @@ namespace osu.Game.Rulesets.Objects return slidingSamples; } + + /// + /// Create a SampleInfo based on the sample settings of the hit normal sample in . + /// + /// The name of the sample. + /// A populated . + protected HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) + { + var hitnormalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + return hitnormalSample == null ? new HitSampleInfo(sampleName) : hitnormalSample.With(newName: sampleName); + } } public static class HitObjectExtensions diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 68ca6bc506..ba5de6c14b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } if (split.Length > 10) - readCustomSampleBanks(split[10], bankInfo); + readCustomSampleBanks(split[10], bankInfo, true); // One node for each repeat + the start and end nodes int nodes = repeatCount + 2; @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Objects.Legacy return result; } - private void readCustomSampleBanks(string str, SampleBankInfo bankInfo) + private void readCustomSampleBanks(string str, SampleBankInfo bankInfo, bool banksOnly = false) { if (string.IsNullOrEmpty(str)) return; @@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Objects.Legacy bankInfo.BankForNormal = stringBank; bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; + if (banksOnly) return; + if (split.Length > 2) bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index bd2713a7d1..7ddd372dc9 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -6,13 +6,14 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -40,13 +41,21 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Velocity = 1; + public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1); + + public double SliderVelocity + { + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; + } + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; } diff --git a/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs b/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs new file mode 100644 index 0000000000..3ac8b8a086 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs @@ -0,0 +1,17 @@ +// 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.Objects.Types +{ + /// + /// A type of which explicitly specifies whether it should generate ticks. + /// + public interface IHasGenerateTicks + { + /// + /// Whether or not slider ticks should be generated by this object. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs new file mode 100644 index 0000000000..c0ac5036ee --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs @@ -0,0 +1,19 @@ +// 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.Bindables; + +namespace osu.Game.Rulesets.Objects.Types; + +/// +/// A HitObject that has a slider velocity multiplier. +/// +public interface IHasSliderVelocity +{ + /// + /// The slider velocity multiplier. + /// + double SliderVelocity { get; set; } + + BindableNumber SliderVelocityBindable { get; } +} diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index e1c03e49e3..d4510a4519 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -52,7 +52,6 @@ namespace osu.Game.Rulesets.UI return; var samples = nextObject.Samples - .Select(s => nextObject.SampleControlPoint.ApplyTo(s)) .Cast() .ToArray(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index b1c3b78e67..6016a53918 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -293,10 +293,10 @@ namespace osu.Game.Rulesets.UI { // prepare sample pools ahead of time so we're not initialising at runtime. foreach (var sample in hitObject.Samples) - prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); + prepareSamplePool(sample); foreach (var sample in hitObject.AuxiliarySamples) - prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); + prepareSamplePool(sample); foreach (var nestedObject in hitObject.NestedHitObjects) preloadSamples(nestedObject); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index d3cdd461ea..4741b75641 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -13,12 +13,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Timing; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -29,13 +31,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly BindableNumber speedMultiplier; public DifficultyPointPiece(HitObject hitObject) - : base(hitObject.DifficultyControlPoint) { HitObject = hitObject; - speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy(); + speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy(); } + protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; + protected override void LoadComplete() { base.LoadComplete(); @@ -78,7 +81,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Spacing = new Vector2(0, 15), Children = new Drawable[] { - sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput("Velocity", new DifficultyControlPoint().SliderVelocityBindable) + sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput("Velocity", new BindableDouble(1) + { + Precision = 0.01, + MinValue = 0.1, + MaxValue = 10 + }) { KeyboardStep = 0.1f }, @@ -94,11 +102,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. - var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray(); + var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray(); // even if there are multiple objects selected, we can still display a value if they all have the same value. - var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null; + var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null; if (selectedPointBindable != null) { @@ -117,7 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in relevantObjects) { - h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value; + ((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value; beatmap.Update(h); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs index 5b0a5729c8..4b357d3a62 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -16,21 +15,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class HitObjectPointPiece : CircularContainer { - private readonly ControlPoint point; - protected OsuSpriteText Label { get; private set; } - protected HitObjectPointPiece(ControlPoint point) - { - this.point = point; - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { AutoSizeAxes = Axes.Both; - Color4 colour = point.GetRepresentingColour(colours); + Color4 colour = GetRepresentingColour(colours); InternalChildren = new Drawable[] { @@ -61,5 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, }; } + + protected virtual Color4 GetRepresentingColour(OsuColour colours) + { + return colours.Yellow; + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 314137a565..b02cfb505e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -12,11 +12,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Audio; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Timing; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -24,22 +26,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly HitObject HitObject; - private readonly Bindable bank; - private readonly BindableNumber volume; + private readonly BindableList samplesBindable; public SamplePointPiece(HitObject hitObject) - : base(hitObject.SampleControlPoint) { HitObject = hitObject; - volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy(); - bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy(); + samplesBindable = hitObject.SamplesBindable.GetBoundCopy(); } + protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink; + [BackgroundDependencyLoader] private void load() { - volume.BindValueChanged(_ => updateText()); - bank.BindValueChanged(_ => updateText(), true); + samplesBindable.BindCollectionChanged((_, _) => updateText(), true); } protected override bool OnClick(ClickEvent e) @@ -50,7 +50,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { - Label.Text = $"{bank.Value} {volume.Value}"; + Label.Text = $"{GetBankValue(samplesBindable)} {GetVolumeValue(samplesBindable)}"; + } + + public static string? GetBankValue(IEnumerable samples) + { + return samples.FirstOrDefault()?.Bank; + } + + public static int GetVolumeValue(ICollection samples) + { + return samples.Count == 0 ? 0 : samples.Max(o => o.Volume); } public Popover GetPopover() => new SampleEditPopover(HitObject); @@ -89,7 +99,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Label = "Bank Name", }, - volume = new IndeterminateSliderWithTextBoxInput("Volume", new SampleControlPoint().SampleVolumeBindable) + volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100) + { + MinValue = 0, + MaxValue = 100, + }) } } }; @@ -100,14 +114,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray(); + var relevantSamples = relevantObjects.Select(h => h.Samples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. - string? commonBank = getCommonBank(relevantControlPoints); + string? commonBank = getCommonBank(relevantSamples); if (!string.IsNullOrEmpty(commonBank)) bank.Current.Value = commonBank; - int? commonVolume = getCommonVolume(relevantControlPoints); + int? commonVolume = getCommonVolume(relevantSamples); if (commonVolume != null) volume.Current.Value = commonVolume.Value; @@ -117,9 +131,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateBankFor(relevantObjects, val.NewValue); updateBankPlaceholderText(relevantObjects); }); - // on commit, ensure that the value is correct by sourcing it from the objects' control points again. + // on commit, ensure that the value is correct by sourcing it from the objects' samples again. // this ensures that committing empty text causes a revert to the previous value. - bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantControlPoints); + bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples); volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); } @@ -130,8 +144,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); } - private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null; - private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? relevantControlPoints.First().SampleVolume : null; + private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; + private static int? getCommonVolume(IList[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; private void updateBankFor(IEnumerable objects, string? newBank) { @@ -142,7 +156,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in objects) { - h.SampleControlPoint.SampleBank = newBank; + for (int i = 0; i < h.Samples.Count; i++) + { + h.Samples[i] = h.Samples[i].With(newBank: newBank); + } + beatmap.Update(h); } @@ -151,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateBankPlaceholderText(IEnumerable objects) { - string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray()); + string? commonBank = getCommonBank(objects.Select(h => h.Samples).ToArray()); bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } @@ -164,7 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in objects) { - h.SampleControlPoint.SampleVolume = newVolume.Value; + for (int i = 0; i < h.Samples.Count; i++) + { + h.Samples[i] = h.Samples[i].With(newVolume: newVolume.Value); + } + beatmap.Update(h); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 4e5087c004..ea063e9216 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -102,6 +101,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, } }, + new SamplePointPiece(Item) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre + }, }); if (item is IHasDuration) @@ -111,6 +115,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled = e => OnDragHandled?.Invoke(e) }); } + + if (item is IHasSliderVelocity) + { + AddInternal(new DifficultyPointPiece(Item) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.BottomCentre + }); + } } protected override void LoadComplete() @@ -187,12 +200,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour); } - private SamplePointPiece? sampleOverrideDisplay; - private DifficultyPointPiece? difficultyOverrideDisplay; - - private DifficultyControlPoint difficultyControlPoint = null!; - private SampleControlPoint sampleControlPoint = null!; - protected override void Update() { base.Update(); @@ -208,36 +215,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (Item is IHasRepeats repeats) updateRepeats(repeats); } - - if (!ReferenceEquals(difficultyControlPoint, Item.DifficultyControlPoint)) - { - difficultyControlPoint = Item.DifficultyControlPoint; - difficultyOverrideDisplay?.Expire(); - - if (Item.DifficultyControlPoint != null && Item is IHasDistance) - { - AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item) - { - Anchor = Anchor.TopLeft, - Origin = Anchor.BottomCentre - }); - } - } - - if (!ReferenceEquals(sampleControlPoint, Item.SampleControlPoint)) - { - sampleControlPoint = Item.SampleControlPoint; - sampleOverrideDisplay?.Expire(); - - if (Item.SampleControlPoint != null) - { - AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopCentre - }); - } - } } private void updateRepeats(IHasRepeats repeats) @@ -395,17 +372,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasRepeats repeatHitObject: double proposedDuration = time - hitObject.StartTime; - if (e.CurrentState.Keyboard.ShiftPressed) + if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity) { - if (ReferenceEquals(hitObject.DifficultyControlPoint, DifficultyControlPoint.DEFAULT)) - hitObject.DifficultyControlPoint = new DifficultyControlPoint(); + double newVelocity = hasSliderVelocity.SliderVelocity * (repeatHitObject.Duration / proposedDuration); - double newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration); - - if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity)) + if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocity)) return; - hitObject.DifficultyControlPoint.SliderVelocity = newVelocity; + hasSliderVelocity.SliderVelocity = newVelocity; beatmap.Update(hitObject); } else @@ -414,7 +388,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount) + if (proposedCount == repeatHitObject.RepeatCount || lengthOfOneRepeat == 0) return; repeatHitObject.RepeatCount = proposedCount; diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index da44161507..2a6ebecb92 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Color4.DarkBlue, - Size = new Vector2(0.96f) + Size = OsuLogo.SCALE_ADJUST, }, new Circle { diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 9430a1cda8..277b8bf888 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -35,6 +35,12 @@ namespace osu.Game.Screens.Menu private const double transition_length = 300; + /// + /// The osu! logo sprite has a shadow included in its texture. + /// This adjustment vector is used to match the precise edge of the border of the logo. + /// + public static readonly Vector2 SCALE_ADJUST = new Vector2(0.96f); + private readonly Sprite logo; private readonly CircularContainer logoContainer; private readonly Container logoBounceContainer; @@ -150,7 +156,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = visualizer_default_alpha, - Size = new Vector2(0.96f) + Size = SCALE_ADJUST }, new Container { @@ -162,7 +168,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.88f), + Scale = SCALE_ADJUST, Masking = true, Children = new Drawable[] { @@ -406,7 +412,7 @@ namespace osu.Game.Screens.Menu public void Impact() { impactContainer.FadeOutFromOne(250, Easing.In); - impactContainer.ScaleTo(0.96f); + impactContainer.ScaleTo(SCALE_ADJUST); impactContainer.ScaleTo(1.12f, 250); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index f065926eb7..0c3de5848b 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Select.Carousel item = value; - if (IsLoaded) + if (IsLoaded && !IsDisposed) UpdateItem(); } } @@ -165,5 +165,13 @@ namespace osu.Game.Screens.Select.Carousel Item.State.Value = CarouselItemState.Selected; return true; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // This is important to clean up event subscriptions. + Item = null; + } } } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 2c16a67cac..6523039a3f 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -84,8 +84,8 @@ namespace osu.Game.Skinning.Components private void updateBeatmapContent(WorkingBeatmap workingBeatmap) { - valueDictionary[BeatmapAttribute.Title] = workingBeatmap.BeatmapInfo.Metadata.Title; - valueDictionary[BeatmapAttribute.Artist] = workingBeatmap.BeatmapInfo.Metadata.Artist; + valueDictionary[BeatmapAttribute.Title] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.TitleUnicode, workingBeatmap.BeatmapInfo.Metadata.Title); + valueDictionary[BeatmapAttribute.Artist] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.ArtistUnicode, workingBeatmap.BeatmapInfo.Metadata.Artist); valueDictionary[BeatmapAttribute.DifficultyName] = workingBeatmap.BeatmapInfo.DifficultyName; valueDictionary[BeatmapAttribute.Creator] = workingBeatmap.BeatmapInfo.Metadata.Author.Username; valueDictionary[BeatmapAttribute.Length] = TimeSpan.FromMilliseconds(workingBeatmap.BeatmapInfo.Length).ToFormattedDuration(); diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index cd9e9e1d52..78188d7cf7 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -3,9 +3,12 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -24,18 +27,27 @@ namespace osu.Game.Tests.Visual protected EditorBeatmap EditorBeatmap => (EditorBeatmap)Editor.Dependencies.Get(typeof(EditorBeatmap)); + [CanBeNull] + protected Func CreateInitialBeatmap { get; set; } + [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + if (CreateInitialBeatmap == null) + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + else + { + AddStep("set test beatmap", () => Game.Beatmap.Value = CreateInitialBeatmap?.Invoke()); + } PushAndConfirm(() => new EditorLoader()); AddUntilStep("wait for editor load", () => Editor?.IsLoaded == true); - AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + if (CreateInitialBeatmap == null) + AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. @@ -50,6 +62,14 @@ namespace osu.Game.Tests.Visual protected void ReloadEditorToSameBeatmap() { + Guid beatmapSetGuid = Guid.Empty; + Guid beatmapGuid = Guid.Empty; + + AddStep("Store beatmap GUIDs", () => + { + beatmapSetGuid = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID; + beatmapGuid = EditorBeatmap.BeatmapInfo.ID; + }); AddStep("Exit", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); @@ -59,7 +79,8 @@ namespace osu.Game.Tests.Visual PushAndConfirm(() => songSelect = new PlaySongSelect()); AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); - AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); + AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid)); + AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid); AddStep("Open options", () => InputManager.Key(Key.F3)); AddStep("Enter editor", () => InputManager.Key(Key.Number5));