diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 7a9b61c60c..0369b6db4e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void load() { var controlPointInfo = new ControlPointInfo(); - controlPointInfo.TimingPoints.Add(new TimingControlPoint()); + controlPointInfo.Add(0, new TimingControlPoint()); WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index b6907cf7c0..a9a6097182 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -64,9 +64,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void Setup() => Schedule(() => { editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; - editorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); - editorBeatmap.ControlPointInfo.TimingPoints.Clear(); - editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length }); + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); }); [TestCase(1)] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 4893ebfdd4..a955911bd5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) { var cpi = new ControlPointInfo(); - cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); + cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 2eb783233a..5f75cbabec 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -313,10 +313,6 @@ namespace osu.Game.Rulesets.Osu.Tests }, 25), } }, - ControlPointInfo = - { - DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } } - }, BeatmapInfo = { BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, @@ -324,6 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests }, }); + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); p.OnLoadComplete += _ => diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index 62c4ba5ee3..7c94568835 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Game.Graphics.Sprites; using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Skinning; @@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - new CircularContainer + new Container { Masking = true, - Origin = Anchor.Centre, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 60, Colour = Color4.White.Opacity(0.5f), }, - Child = new Box() }, number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index eaa8ca7ebb..8522a42739 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Reset height", () => changePlayfieldSize(6)); var controlPointInfo = new ControlPointInfo(); - controlPointInfo.TimingPoints.Add(new TimingControlPoint()); + controlPointInfo.Add(0, new TimingControlPoint()); WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap { @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.Tests HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; var cpi = new ControlPointInfo(); - cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); + cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Taiko.Tests HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; var cpi = new ControlPointInfo(); - cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); + cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs index ad2596931d..aaf113f216 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs @@ -19,12 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Audio { this.controlPoints = controlPoints; - IEnumerable samplePoints; - if (controlPoints.SamplePoints.Count == 0) - // Get the default sample point - samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) }; - else - samplePoints = controlPoints.SamplePoints; + IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; foreach (var s in samplePoints) { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index de516d3142..2ecc516919 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -167,9 +167,9 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = beatmap.ControlPointInfo; Assert.AreEqual(4, controlPoints.TimingPoints.Count); - Assert.AreEqual(42, controlPoints.DifficultyPoints.Count); - Assert.AreEqual(42, controlPoints.SamplePoints.Count); - Assert.AreEqual(42, controlPoints.EffectPoints.Count); + Assert.AreEqual(5, controlPoints.DifficultyPoints.Count); + Assert.AreEqual(34, controlPoints.SamplePoints.Count); + Assert.AreEqual(8, controlPoints.EffectPoints.Count); var timingPoint = controlPoints.TimingPointAt(0); Assert.AreEqual(956, timingPoint.Time); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); difficultyPoint = controlPoints.DifficultyPointAt(48428); - Assert.AreEqual(48428, difficultyPoint.Time); + Assert.AreEqual(0, difficultyPoint.Time); Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); difficultyPoint = controlPoints.DifficultyPointAt(116999); @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsFalse(effectPoint.OmitFirstBarLine); effectPoint = controlPoints.EffectPointAt(119637); - Assert.AreEqual(119637, effectPoint.Time); + Assert.AreEqual(95901, effectPoint.Time); Assert.IsFalse(effectPoint.KiaiMode); Assert.IsFalse(effectPoint.OmitFirstBarLine); } @@ -262,6 +262,21 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestTimingPointResetsSpeedMultiplier() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var controlPoints = decoder.Decode(stream).ControlPointInfo; + + Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1)); + } + } + [Test] public void TestDecodeBeatmapColours() { @@ -362,6 +377,23 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeControlPointDifficultyChange() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var controlPointInfo = decoder.Decode(stream).ControlPointInfo; + + Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1)); + Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10)); + Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d)); + Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5)); + } + } + [Test] public void TestDecodeControlPointCustomSampleBank() { diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs index e6b0cf992a..fe3cc375ea 100644 --- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs @@ -25,11 +25,10 @@ namespace osu.Game.Tests.Editor BeatDivisor.Value = 1; composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; - composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = 1 }); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 1000 }); + composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 }); + composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); }); [TestCase(1)] @@ -47,8 +46,8 @@ namespace osu.Game.Tests.Editor { AddStep($"set multiplier = {multiplier}", () => { - composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = multiplier }); + composer.EditorBeatmap.ControlPointInfo.Clear(); + composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier }); }); assertSnapDistance(100 * multiplier); @@ -76,8 +75,8 @@ namespace osu.Game.Tests.Editor AddStep("set beat length = 500", () => { - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + composer.EditorBeatmap.ControlPointInfo.Clear(); + composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); }); assertDurationToDistance(500, 200); @@ -97,8 +96,8 @@ namespace osu.Game.Tests.Editor AddStep("set beat length = 500", () => { - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + composer.EditorBeatmap.ControlPointInfo.Clear(); + composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); }); assertDistanceToDuration(200, 500); @@ -124,8 +123,8 @@ namespace osu.Game.Tests.Editor AddStep("set beat length = 500", () => { - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + composer.EditorBeatmap.ControlPointInfo.Clear(); + composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); }); assertSnappedDuration(50, 0); @@ -155,8 +154,8 @@ namespace osu.Game.Tests.Editor AddStep("set beat length = 500", () => { - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); - composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + composer.EditorBeatmap.ControlPointInfo.Clear(); + composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); }); assertSnappedDistance(50, 0); diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs new file mode 100644 index 0000000000..a51b90851c --- /dev/null +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -0,0 +1,227 @@ +// 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.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class ControlPointInfoTest + { + [Test] + public void TestAdd() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new TimingControlPoint()); + cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); + + Assert.That(cpi.Groups.Count, Is.EqualTo(2)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); + } + + [Test] + public void TestAddRedundantTiming() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point. + cpi.Add(1000, new TimingControlPoint()); // is redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + } + + [Test] + public void TestAddRedundantDifficulty() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new DifficultyControlPoint()); // is redundant + cpi.Add(1000, new DifficultyControlPoint()); // is redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); + + cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + } + + [Test] + public void TestAddRedundantSample() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new SampleControlPoint()); // is redundant + cpi.Add(1000, new SampleControlPoint()); // is redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); + + cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + } + + [Test] + public void TestAddRedundantEffect() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new EffectControlPoint()); // is redundant + cpi.Add(1000, new EffectControlPoint()); // is redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); + + cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + } + + [Test] + public void TestAddGroup() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + var group2 = cpi.GroupAt(1000, true); + + Assert.That(group, Is.EqualTo(group2)); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + } + + [Test] + public void TestGroupAtLookupOnly() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(5000, true); + Assert.That(group, Is.Not.Null); + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.GroupAt(1000), Is.Null); + Assert.That(cpi.GroupAt(5000), Is.Not.Null); + } + + [Test] + public void TestAddRemoveGroup() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + + cpi.RemoveGroup(group); + + Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + } + + [Test] + public void TestAddControlPointToGroup() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + + // usually redundant, but adding to group forces it to be added + group.Add(new DifficultyControlPoint()); + + Assert.That(group.ControlPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); + } + + [Test] + public void TestAddDuplicateControlPointToGroup() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + + group.Add(new DifficultyControlPoint()); + group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 }); + + Assert.That(group.ControlPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2)); + } + + [Test] + public void TestRemoveControlPointFromGroup() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + + var difficultyPoint = new DifficultyControlPoint(); + + group.Add(difficultyPoint); + group.Remove(difficultyPoint); + + Assert.That(group.ControlPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0)); + } + + [Test] + public void TestOrdering() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new TimingControlPoint()); + cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); + cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); + cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); + cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); + cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); + + Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(8)); + + Assert.That(cpi.Groups, Is.Ordered.Ascending.By(nameof(ControlPointGroup.Time))); + + Assert.That(cpi.AllControlPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time))); + Assert.That(cpi.TimingPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time))); + } + + [Test] + public void TestClear() + { + var cpi = new ControlPointInfo(); + + cpi.Add(0, new TimingControlPoint()); + cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); + cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); + cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); + cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); + cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); + + cpi.Clear(); + + Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0)); + } + } +} diff --git a/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu new file mode 100644 index 0000000000..5f06fc33c8 --- /dev/null +++ b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu @@ -0,0 +1,8 @@ +osu file format v7 + +[TimingPoints] +0,100,4,2,0,100,1,0 +12,500,4,2,0,100,1,0 +1000,-10,4,2,0,100,0,0 +2000,-54,4,2,0,100,0,0 +3000,-200,4,2,0,100,0,0 diff --git a/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu new file mode 100644 index 0000000000..4512903c68 --- /dev/null +++ b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu @@ -0,0 +1,5 @@ +osu file format v14 + +[TimingPoints] +0,-200,4,1,0,100,0,0 +2000,100,1,1,0,100,1,0 diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs index 558e1106a7..b8c31d5dbb 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Editor public TestSceneDistanceSnapGrid() { editorBeatmap = new EditorBeatmap(new OsuBeatmap()); - editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length }); + editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs index b997d6aaeb..3118e0cabe 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs @@ -28,18 +28,7 @@ namespace osu.Game.Tests.Visual.Editor { var testBeatmap = new Beatmap { - ControlPointInfo = new ControlPointInfo - { - TimingPoints = - { - new TimingControlPoint { Time = 0, BeatLength = 200 }, - new TimingControlPoint { Time = 100, BeatLength = 400 }, - new TimingControlPoint { Time = 175, BeatLength = 800 }, - new TimingControlPoint { Time = 350, BeatLength = 200 }, - new TimingControlPoint { Time = 450, BeatLength = 100 }, - new TimingControlPoint { Time = 500, BeatLength = 307.69230769230802 } - } - }, + ControlPointInfo = new ControlPointInfo(), HitObjects = { new HitCircle { StartTime = 0 }, @@ -47,6 +36,13 @@ namespace osu.Game.Tests.Visual.Editor } }; + testBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 }); + testBeatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 400 }); + testBeatmap.ControlPointInfo.Add(175, new TimingControlPoint { BeatLength = 800 }); + testBeatmap.ControlPointInfo.Add(350, new TimingControlPoint { BeatLength = 200 }); + testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 }); + testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 }); + Beatmap.Value = CreateWorkingBeatmap(testBeatmap); Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index dcab964d6d..684e79b3f5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -47,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestRelativeBeatLengthScaleSingleTimingPoint() { - var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 }); createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); @@ -61,10 +62,10 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant() { - var beatmap = createBeatmap( - new TimingControlPoint { BeatLength = time_range / 2 }, - new TimingControlPoint { Time = 12000, BeatLength = time_range }, - new TimingControlPoint { Time = 100000, BeatLength = time_range }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 }); + beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range }); + beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range }); createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); @@ -75,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestRelativeBeatLengthScaleFromSecondTimingPoint() { - var beatmap = createBeatmap( - new TimingControlPoint { BeatLength = time_range }, - new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 }); createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); @@ -97,9 +98,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNonRelativeScale() { - var beatmap = createBeatmap( - new TimingControlPoint { BeatLength = time_range }, - new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 }); createTest(beatmap); @@ -119,7 +120,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestSliderMultiplierDoesNotAffectRelativeBeatLength() { - var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); @@ -132,7 +134,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestSliderMultiplierAffectsNonRelativeBeatLength() { - var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range }); + var beatmap = createBeatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; createTest(beatmap); @@ -154,14 +157,11 @@ namespace osu.Game.Tests.Visual.Gameplay /// Creates an , containing 10 hitobjects and user-provided timing points. /// The hitobjects are spaced milliseconds apart. /// - /// The timing points to add to the beatmap. /// The . - private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints) + private IBeatmap createBeatmap() { var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; - beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints); - for (int i = 0; i < 10; i++) beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index d973799405..01400bf1d9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -9,6 +9,7 @@ using osuTK; using System; using System.Linq; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online @@ -137,6 +138,17 @@ namespace osu.Game.Tests.Visual.Online }); }, Channel.MAX_HISTORY / messages_per_call + 5); + AddAssert("Ensure no adjacent day separators", () => + { + var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds)); + + foreach (var i in indices) + if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator) + return false; + + return true; + }); + AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); } @@ -147,7 +159,13 @@ namespace osu.Game.Tests.Visual.Online { } - public bool ScrolledToBottom => ((ScrollContainer)((Container)InternalChildren.OfType().First().Child).Child).IsScrolledToEnd(1); + protected DrawableChannel DrawableChannel => InternalChildren.OfType().First(); + + protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child; + + public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child; + + public bool ScrolledToBottom => ScrollContainer.IsScrolledToEnd(1); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index d84ffa0d93..ed44d82bce 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -10,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Lists; using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } - private SortedList timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints; + private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList(); private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 0081fab46a..0861e00d8d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -10,12 +10,11 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time at which the control point takes effect. /// - public double Time; + public double Time => controlPointGroup?.Time ?? 0; - /// - /// Whether this timing point was generated internally, as opposed to parsed from the underlying beatmap. - /// - internal bool AutoGenerated; + private ControlPointGroup controlPointGroup; + + public void AttachGroup(ControlPointGroup pointGroup) => this.controlPointGroup = pointGroup; public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs new file mode 100644 index 0000000000..cb73ce884e --- /dev/null +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs @@ -0,0 +1,50 @@ +// 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.Linq; +using osu.Framework.Bindables; + +namespace osu.Game.Beatmaps.ControlPoints +{ + public class ControlPointGroup : IComparable + { + public event Action ItemAdded; + public event Action ItemRemoved; + + /// + /// The time at which the control point takes effect. + /// + public double Time { get; } + + public IBindableList ControlPoints => controlPoints; + + private readonly BindableList controlPoints = new BindableList(); + + public ControlPointGroup(double time) + { + Time = time; + } + + public int CompareTo(ControlPointGroup other) => Time.CompareTo(other.Time); + + public void Add(ControlPoint point) + { + var existing = controlPoints.FirstOrDefault(p => p.GetType() == point.GetType()); + + if (existing != null) + Remove(existing); + + point.AttachGroup(this); + + controlPoints.Add(point); + ItemAdded?.Invoke(point); + } + + public void Remove(ControlPoint point) + { + controlPoints.Remove(point); + ItemRemoved?.Invoke(point); + } + } +} diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 855084ad02..c3e2b469ae 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Lists; namespace osu.Game.Beatmaps.ControlPoints @@ -12,57 +13,78 @@ namespace osu.Game.Beatmaps.ControlPoints [Serializable] public class ControlPointInfo { + /// + /// All control points grouped by time. + /// + [JsonProperty] + public IBindableList Groups => groups; + + private readonly BindableList groups = new BindableList(); + /// /// All timing points. /// [JsonProperty] - public SortedList TimingPoints { get; private set; } = new SortedList(Comparer.Default); + public IReadOnlyList TimingPoints => timingPoints; + + private readonly SortedList timingPoints = new SortedList(Comparer.Default); /// /// All difficulty points. /// [JsonProperty] - public SortedList DifficultyPoints { get; private set; } = new SortedList(Comparer.Default); + public IReadOnlyList DifficultyPoints => difficultyPoints; + + private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); /// /// All sound points. /// [JsonProperty] - public SortedList SamplePoints { get; private set; } = new SortedList(Comparer.Default); + public IReadOnlyList SamplePoints => samplePoints; + + private readonly SortedList samplePoints = new SortedList(Comparer.Default); /// /// All effect points. /// [JsonProperty] - public SortedList EffectPoints { get; private set; } = new SortedList(Comparer.Default); + public IReadOnlyList EffectPoints => effectPoints; + + private readonly SortedList effectPoints = new SortedList(Comparer.Default); + + /// + /// All control points, of all types. + /// + public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); /// /// Finds the difficulty control point that is active at . /// /// The time to find the difficulty control point at. /// The difficulty control point. - public DifficultyControlPoint DifficultyPointAt(double time) => binarySearch(DifficultyPoints, time); + public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time); /// /// Finds the effect control point that is active at . /// /// The time to find the effect control point at. /// The effect control point. - public EffectControlPoint EffectPointAt(double time) => binarySearch(EffectPoints, time); + public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time); /// /// Finds the sound control point that is active at . /// /// The time to find the sound control point at. /// The sound control point. - public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null); + public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null); /// /// Finds the timing control point that is active at . /// /// The time to find the timing control point at. /// The timing control point. - public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null); + public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null); /// /// Finds the maximum BPM represented by any timing control point. @@ -85,24 +107,93 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPMMode => 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + /// + /// Remove all s and return to a pristine state. + /// + public void Clear() + { + groups.Clear(); + timingPoints.Clear(); + difficultyPoints.Clear(); + samplePoints.Clear(); + effectPoints.Clear(); + } + + /// + /// Add a new . Note that the provided control point may not be added if the correct state is already present at the provided time. + /// + /// The time at which the control point should be added. + /// The control point to add. + /// Whether the control point was added. + public bool Add(double time, ControlPoint controlPoint) + { + if (checkAlreadyExisting(time, controlPoint)) + return false; + + GroupAt(time, true).Add(controlPoint); + return true; + } + + public ControlPointGroup GroupAt(double time, bool addIfNotExisting = false) + { + var newGroup = new ControlPointGroup(time); + + int i = groups.BinarySearch(newGroup); + + if (i >= 0) + return groups[i]; + + if (addIfNotExisting) + { + newGroup.ItemAdded += groupItemAdded; + newGroup.ItemRemoved += groupItemRemoved; + + groups.Insert(~i, newGroup); + return newGroup; + } + + return null; + } + + public void RemoveGroup(ControlPointGroup group) + { + group.ItemAdded -= groupItemAdded; + group.ItemRemoved -= groupItemRemoved; + + groups.Remove(group); + } + /// /// Binary searches one of the control point lists to find the active control point at . + /// Includes logic for returning a specific point when no matching point is found. /// /// The list to search. /// The time to find the control point at. /// The control point to use when is before any control points. If null, a new control point will be constructed. - /// The active control point at . - private T binarySearch(SortedList list, double time, T prePoint = null) + /// The active control point at , or a fallback if none found. + private T binarySearchWithFallback(IReadOnlyList list, double time, T prePoint = null) where T : ControlPoint, new() + { + return binarySearch(list, time) ?? prePoint ?? new T(); + } + + /// + /// Binary searches one of the control point lists to find the active control point at . + /// + /// The list to search. + /// The time to find the control point at. + /// The active control point at . + private T binarySearch(IReadOnlyList list, double time) + where T : ControlPoint { if (list == null) throw new ArgumentNullException(nameof(list)); if (list.Count == 0) - return new T(); + return null; if (time < list[0].Time) - return prePoint ?? new T(); + return null; if (time >= list[list.Count - 1].Time) return list[list.Count - 1]; @@ -125,5 +216,82 @@ namespace osu.Game.Beatmaps.ControlPoints // l will be the first control point with Time > time, but we want the one before it return list[l - 1]; } + + /// + /// Check whether should be added. + /// + /// The time to find the timing control point at. + /// A point to be added. + /// Whether the new point should be added. + private bool checkAlreadyExisting(double time, ControlPoint newPoint) + { + ControlPoint existing = null; + + switch (newPoint) + { + case TimingControlPoint _: + // Timing points are a special case and need to be added regardless of fallback availability. + existing = binarySearch(TimingPoints, time); + break; + + case EffectControlPoint _: + existing = EffectPointAt(time); + break; + + case SampleControlPoint _: + existing = SamplePointAt(time); + break; + + case DifficultyControlPoint _: + existing = DifficultyPointAt(time); + break; + } + + return existing?.EquivalentTo(newPoint) == true; + } + + private void groupItemAdded(ControlPoint controlPoint) + { + switch (controlPoint) + { + case TimingControlPoint typed: + timingPoints.Add(typed); + break; + + case EffectControlPoint typed: + effectPoints.Add(typed); + break; + + case SampleControlPoint typed: + samplePoints.Add(typed); + break; + + case DifficultyControlPoint typed: + difficultyPoints.Add(typed); + break; + } + } + + private void groupItemRemoved(ControlPoint controlPoint) + { + switch (controlPoint) + { + case TimingControlPoint typed: + timingPoints.Remove(typed); + break; + + case EffectControlPoint typed: + effectPoints.Remove(typed); + break; + + case SampleControlPoint typed: + samplePoints.Remove(typed); + break; + + case DifficultyControlPoint typed: + difficultyPoints.Remove(typed); + break; + } + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 42651fd0ca..8b21098a51 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -1,24 +1,33 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; +using osu.Framework.Bindables; namespace osu.Game.Beatmaps.ControlPoints { public class DifficultyControlPoint : ControlPoint { + /// + /// The speed multiplier at this control point. + /// + public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1) + { + Precision = 0.1, + Default = 1, + MinValue = 0.1, + MaxValue = 10 + }; + /// /// The speed multiplier at this control point. /// public double SpeedMultiplier { - get => speedMultiplier; - set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10); + get => SpeedMultiplierBindable.Value; + set => SpeedMultiplierBindable.Value = value; } - private double speedMultiplier = 1; - public override bool EquivalentTo(ControlPoint other) => - other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(speedMultiplier); + other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier); } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 928f2a51ad..369b93ff3d 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -1,19 +1,39 @@ // 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.Beatmaps.ControlPoints { public class EffectControlPoint : ControlPoint { /// - /// Whether this control point enables Kiai mode. + /// Whether the first bar line of this control point is ignored. /// - public bool KiaiMode; + public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); /// /// Whether the first bar line of this control point is ignored. /// - public bool OmitFirstBarLine; + public bool OmitFirstBarLine + { + get => OmitFirstBarLineBindable.Value; + set => OmitFirstBarLineBindable.Value = value; + } + + /// + /// Whether this control point enables Kiai mode. + /// + public readonly BindableBool KiaiModeBindable = new BindableBool(); + + /// + /// Whether this control point enables Kiai mode. + /// + public bool KiaiMode + { + get => KiaiModeBindable.Value; + set => KiaiModeBindable.Value = value; + } public override bool EquivalentTo(ControlPoint other) => other is EffectControlPoint otherTyped && diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 07f5aa6c90..42865c686c 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.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 osu.Framework.Bindables; using osu.Game.Audio; namespace osu.Game.Beatmaps.ControlPoints @@ -12,12 +13,35 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The default sample bank at this control point. /// - public string SampleBank = DEFAULT_BANK; + public readonly Bindable SampleBankBindable = new Bindable(DEFAULT_BANK) { Default = DEFAULT_BANK }; + + /// + /// The speed multiplier at this control point. + /// + public string SampleBank + { + get => SampleBankBindable.Value; + set => SampleBankBindable.Value = value; + } + + /// + /// The default sample bank at this control point. + /// + public readonly BindableInt SampleVolumeBindable = new BindableInt(100) + { + MinValue = 0, + MaxValue = 100, + Default = 100 + }; /// /// The default sample volume at this control point. /// - public int SampleVolume = 100; + public int SampleVolume + { + get => SampleVolumeBindable.Value; + set => SampleVolumeBindable.Value = value; + } /// /// Create a SampleInfo based on the sample settings in this control point. diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index f8c84c79dd..c00b04b660 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -1,7 +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 osuTK; +using osu.Framework.Bindables; using osu.Game.Beatmaps.Timing; namespace osu.Game.Beatmaps.ControlPoints @@ -11,23 +11,40 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time signature at this control point. /// - public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple; + public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple }; + + /// + /// The time signature at this control point. + /// + public TimeSignatures TimeSignature + { + get => TimeSignatureBindable.Value; + set => TimeSignatureBindable.Value = value; + } public const double DEFAULT_BEAT_LENGTH = 1000; /// /// The beat length at this control point. /// - public virtual double BeatLength + public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH) { - get => beatLength; - set => beatLength = MathHelper.Clamp(value, 6, 60000); - } + Default = DEFAULT_BEAT_LENGTH, + MinValue = 6, + MaxValue = 60000 + }; - private double beatLength = DEFAULT_BEAT_LENGTH; + /// + /// The beat length at this control point. + /// + public double BeatLength + { + get => BeatLengthBindable.Value; + set => BeatLengthBindable.Value = value; + } public override bool EquivalentTo(ControlPoint other) => other is TimingControlPoint otherTyped - && TimeSignature == otherTyped.TimeSignature && beatLength.Equals(otherTyped.beatLength); + && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 786b7611b5..aeb5df46f8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.IO.File; @@ -50,6 +51,8 @@ namespace osu.Game.Beatmaps.Formats base.ParseStreamInto(stream, beatmap); + flushPendingPoints(); + // Objects may be out of order *only* if a user has manually edited an .osu file. // Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828). // OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted) @@ -369,104 +372,64 @@ namespace osu.Game.Beatmaps.Formats if (timingChange) { var controlPoint = CreateTimingControlPoint(); - controlPoint.Time = time; + controlPoint.BeatLength = beatLength; controlPoint.TimeSignature = timeSignature; - handleTimingControlPoint(controlPoint); + addControlPoint(time, controlPoint, true); } - handleDifficultyControlPoint(new DifficultyControlPoint + addControlPoint(time, new LegacyDifficultyControlPoint { - Time = time, SpeedMultiplier = speedMultiplier, - AutoGenerated = timingChange - }); + }, timingChange); - handleEffectControlPoint(new EffectControlPoint + addControlPoint(time, new EffectControlPoint { - Time = time, KiaiMode = kiaiMode, OmitFirstBarLine = omitFirstBarSignature, - AutoGenerated = timingChange - }); + }, timingChange); - handleSampleControlPoint(new LegacySampleControlPoint + addControlPoint(time, new LegacySampleControlPoint { - Time = time, SampleBank = stringSampleSet, SampleVolume = sampleVolume, CustomSampleBank = customSampleBank, - AutoGenerated = timingChange - }); + }, timingChange); + + // To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but + // appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line + // with the same time value (allowing them to overwrite as necessary). + // + // The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal. + if (timingChange) + flushPendingPoints(); } - private void handleTimingControlPoint(TimingControlPoint newPoint) + private readonly List pendingControlPoints = new List(); + private double pendingControlPointsTime; + + private void addControlPoint(double time, ControlPoint point, bool timingChange) { - var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time); + if (time != pendingControlPointsTime) + flushPendingPoints(); - if (existing.Time == newPoint.Time) + if (timingChange) { - // autogenerated points should not replace non-autogenerated. - // this allows for incorrectly ordered timing points to still be correctly handled. - if (newPoint.AutoGenerated && !existing.AutoGenerated) - return; - - beatmap.ControlPointInfo.TimingPoints.Remove(existing); + beatmap.ControlPointInfo.Add(time, point); + return; } - beatmap.ControlPointInfo.TimingPoints.Add(newPoint); + pendingControlPoints.Add(point); + pendingControlPointsTime = time; } - private void handleDifficultyControlPoint(DifficultyControlPoint newPoint) + private void flushPendingPoints() { - var existing = beatmap.ControlPointInfo.DifficultyPointAt(newPoint.Time); + foreach (var p in pendingControlPoints) + beatmap.ControlPointInfo.Add(pendingControlPointsTime, p); - if (existing.Time == newPoint.Time) - { - // autogenerated points should not replace non-autogenerated. - // this allows for incorrectly ordered timing points to still be correctly handled. - if (newPoint.AutoGenerated && !existing.AutoGenerated) - return; - - beatmap.ControlPointInfo.DifficultyPoints.Remove(existing); - } - - beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint); - } - - private void handleEffectControlPoint(EffectControlPoint newPoint) - { - var existing = beatmap.ControlPointInfo.EffectPointAt(newPoint.Time); - - if (existing.Time == newPoint.Time) - { - // autogenerated points should not replace non-autogenerated. - // this allows for incorrectly ordered timing points to still be correctly handled. - if (newPoint.AutoGenerated && !existing.AutoGenerated) - return; - - beatmap.ControlPointInfo.EffectPoints.Remove(existing); - } - - beatmap.ControlPointInfo.EffectPoints.Add(newPoint); - } - - private void handleSampleControlPoint(SampleControlPoint newPoint) - { - var existing = beatmap.ControlPointInfo.SamplePointAt(newPoint.Time); - - if (existing.Time == newPoint.Time) - { - // autogenerated points should not replace non-autogenerated. - // this allows for incorrectly ordered timing points to still be correctly handled. - if (newPoint.AutoGenerated && !existing.AutoGenerated) - return; - - beatmap.ControlPointInfo.SamplePoints.Remove(existing); - } - - beatmap.ControlPointInfo.SamplePoints.Add(newPoint); + pendingControlPoints.Clear(); } private void handleHitObject(string line) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index a5a4380d4a..2b914669cb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -189,6 +189,14 @@ namespace osu.Game.Beatmaps.Formats Foreground = 3 } + internal class LegacyDifficultyControlPoint : DifficultyControlPoint + { + public LegacyDifficultyControlPoint() + { + SpeedMultiplierBindable.Precision = double.Epsilon; + } + } + internal class LegacySampleControlPoint : SampleControlPoint { public int CustomSampleBank; diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 238187bf8f..527f520172 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -28,11 +28,15 @@ namespace osu.Game.Beatmaps.Formats } protected override TimingControlPoint CreateTimingControlPoint() - => new LegacyDifficultyCalculatorControlPoint(); + => new LegacyDifficultyCalculatorTimingControlPoint(); - private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint + private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint { - public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH; + public LegacyDifficultyCalculatorTimingControlPoint() + { + BeatLengthBindable.MinValue = double.MinValue; + BeatLengthBindable.MaxValue = double.MaxValue; + } } } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 370d044ba4..2e76ab964f 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -104,14 +104,10 @@ namespace osu.Game.Graphics.Containers defaultTiming = new TimingControlPoint { BeatLength = default_beat_length, - AutoGenerated = true, - Time = 0 }; defaultEffect = new EffectControlPoint { - Time = 0, - AutoGenerated = true, KiaiMode = false, OmitFirstBarLine = false }; diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index c1810800a0..4124d2ad58 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { hover.FadeIn(200); - return base.OnHover(e); + return true; } protected override void OnHoverLost(HoverLostEvent e) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 9301daa9ff..427bd8dcde 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -108,11 +108,25 @@ namespace osu.Game.Overlays.Chat var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); int count = staleMessages.Length - Channel.MAX_HISTORY; - for (int i = 0; i < count; i++) + if (count > 0) { - var d = staleMessages[i]; - scroll.OffsetScrollPosition(-d.DrawHeight); - d.Expire(); + void expireAndAdjustScroll(Drawable d) + { + scroll.OffsetScrollPosition(-d.DrawHeight); + d.Expire(); + } + + for (int i = 0; i < count; i++) + expireAndAdjustScroll(staleMessages[i]); + + // remove all adjacent day separators after stale message removal + for (int i = 0; i < ChatLineFlow.Count - 1; i++) + { + if (!(ChatLineFlow[i] is DaySeparator)) break; + if (!(ChatLineFlow[i + 1] is DaySeparator)) break; + + expireAndAdjustScroll(ChatLineFlow[i]); + } } if (shouldScrollToEnd) @@ -142,7 +156,7 @@ namespace osu.Game.Overlays.Chat private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); - protected class DaySeparator : Container + public class DaySeparator : Container { public float TextSize { diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index b02b1a5489..a8bbccb168 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -200,6 +200,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { private TextBox username; private TextBox password; + private ShakeContainer shakeSignIn; private IAPIProvider api; public Action RequestHide; @@ -208,6 +209,8 @@ namespace osu.Game.Overlays.Settings.Sections.General { if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) api.Login(username.Text, password.Text); + else + shakeSignIn.Shake(); } [BackgroundDependencyLoader(permitNulls: true)] @@ -244,10 +247,23 @@ namespace osu.Game.Overlays.Settings.Sections.General LabelText = "Stay signed in", Bindable = config.GetBindable(OsuSetting.SavePassword), }, - new SettingsButton + new Container { - Text = "Sign in", - Action = performLogin + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + shakeSignIn = new ShakeContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SettingsButton + { + Text = "Sign in", + Action = performLogin + }, + } + } }, new SettingsButton { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 24fb561f04..bd2db4ae2b 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to // the next timing point's start time - var nextTimingPoint = ControlPointInfo.TimingPoints.Find(t => t.Time > timingPoint.Time); + var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); if (position > nextTimingPoint?.Time) position = nextTimingPoint.Time; @@ -85,12 +85,8 @@ namespace osu.Game.Screens.Edit var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime); if (direction < 0 && timingPoint.Time == CurrentTime) - { // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into - int activeIndex = ControlPointInfo.TimingPoints.IndexOf(timingPoint); - while (activeIndex > 0 && CurrentTime == timingPoint.Time) - timingPoint = ControlPointInfo.TimingPoints[--activeIndex]; - } + timingPoint = ControlPointInfo.TimingPointAt(CurrentTime - 1); double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount; double seekTime = CurrentTime + seekAmount * direction; @@ -124,7 +120,7 @@ namespace osu.Game.Screens.Edit if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) seekTime = timingPoint.Time; - var nextTimingPoint = ControlPointInfo.TimingPoints.Find(t => t.Time > timingPoint.Time); + var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); if (seekTime > nextTimingPoint?.Time) seekTime = nextTimingPoint.Time;